diff --git a/application/pom.xml b/application/pom.xml index 98b73133b6..9cc4ca8dd5 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT thingsboard application diff --git a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json index ec1341cc71..de71ced3a5 100644 --- a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json +++ b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json @@ -49,8 +49,11 @@ "name": "Save Client Attributes", "debugMode": false, "configuration": { + "version": 1, "scope": "CLIENT_SCOPE", - "notifyDevice": "false" + "notifyDevice": "false", + "sendAttributesUpdatedNotification": "false", + "updateAttributesOnlyOnValueChange": "true" }, "externalId": null }, diff --git a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json index 04f4042fae..42cbd9b262 100644 --- a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json @@ -7,28 +7,7 @@ "externalId": null, "name": "Alarm widgets" }, - "widgetTypes": [ - { - "alias": "alarms_table", - "name": "Alarms table", - "image": "data:image/png;base64,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", - "description": "Displays alarms based on defined time window and other filters.", - "descriptor": { - "type": "alarm", - "sizeX": 10.5, - "sizeY": 6.5, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-alarms-table-widget-settings", - "dataKeySettingsDirective": "tb-alarms-table-key-settings", - "hasBasicMode": true, - "basicModeDirective": "tb-alarms-table-basic-config", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true,\"entitiesTitle\":null,\"alarmsTitle\":\"Alarms\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"warning\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false,\"configMode\":\"basic\",\"alarmFilterConfig\":null}" - } - } + "widgetTypeFqns": [ + "alarm_widgets.alarms_table" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/analogue_gauges.json b/application/src/main/data/json/system/widget_bundles/analogue_gauges.json index b2ae314324..04231563fd 100644 --- a/application/src/main/data/json/system/widget_bundles/analogue_gauges.json +++ b/application/src/main/data/json/system/widget_bundles/analogue_gauges.json @@ -2,104 +2,16 @@ "widgetsBundle": { "alias": "analogue_gauges", "title": "Analogue gauges", - "image": "data:image/png;base64,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", - "description": "Display temperature, humidity, speed, and other latest values on various analog gauge widgets." + "image": "data:image/png;base64,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", + "description": "Display temperature, humidity, speed, and other latest values on various analog gauge widgets.", + "externalId": null, + "name": "Analogue gauges" }, - "widgetTypes": [ - { - "alias": "analogue_compass", - "name": "Compass", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAh70lEQVR42u2d+b9V0//H799yJTKTIUPGMiXKGJlSxjJkqkwJCSGRhPDVTESKjImKkCZRKZkyD6mIhL7Pz3k9PuvztvY++5zb2WcP9+73D/ex7xn22eu9Xus9r/dq2FLQli3//PPPn3/+uXHjxvXr1//8888//PDDd9999/XXX69Zs+aLEn3++eeflYgLvcJbfICP8WG+whf5OjfhVgU/oYaWOey///4bHPzyyy/ff//9V1999VmsxA25LTfnJ/ihAljNH0y//vrrjz/+GDuSKuKMH+WnWxTImj+wUE8Ij2+++eazDBCPwcPwSAWw8o2nKoXT6tWr33rrraeffvr++++/4YYbLrjggtNOO+2YY45p3779brvttuuuu+64446NJeKCf3mRt/gAH+PDfGXkyJFTpkx5++23uVWVYqx5I6y5AQvb+bfffvv222+j5/WTTz557bXXhg8ffskllxx99NEON7UTt+rUqRO3vffee2fOnMkPRT8JHsCGDRuan8nffIC1efNmvDP8tXJT+Omnn7766qtDhgw5+eSTY0RSNO20006nnHIKPwqOeYByz8Zj8/AMoQBWhmjTpk34/BHC6cknn+zdu/dee+3VmCrtvffeffr0mTx5cjkxRiyDgTCcAljpG1I49uXk09SpU5lI7KGtw0GrVq34u8022+y+++5cHFMiLviXF90Hqqc2rVvfe9JJXfbZh0dCVz733HPlZBiqPO/wamh+kPrggw9GjBhx2GGHNWnW9913X9QWF+ecc45e6devH39bt2595ZVX6l+9wr/bbrut+wDUs2dPaT1uEvETvQ4++Jcbb3yzd2/3Ch4AWnLRokWhA2GA+bXu8wcsokGYIy4UbmnOnDnMeps2bapB0n777Sc0nHvuuVwce+yxEkhXXHHFdtttZ3GjCwes/v37e8i7/PLLJdK4CRfcUBjVTziacOaZAOvGzp29J+EOuJavv/56qHIkBpbHAFjOgEXmJNQ8nzZtGjayNFQ0nXDCCVJhAwYM0CsCyp577nneeedxcfbZZwsQ5YDlvc6HzzrrLC7OP//8tm3bWuTpJ/g5frR1q1ZfDBgAsA7cddfQB+Phu3XrNn369FDTHs+xAFa9nL7QIMKMGTO6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", - "description": "Displays latest value of the attribute or timeseries key on the compass. Expects value to be in range of 0 to 360.", - "descriptor": { - "type": "latest", - "sizeX": 6, - "sizeY": 5, - "resources": [], - "templateHtml": "", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueCompass(self.ctx, 'compass');\n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-analogue-compass-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"minorTicks\":22,\"needleCircleSize\":15,\"showBorder\":true,\"borderOuterWidth\":10,\"colorPlate\":\"#222\",\"colorMajorTicks\":\"#f5f5f5\",\"colorMinorTicks\":\"#ddd\",\"colorNeedle\":\"#f08080\",\"colorNeedleCircle\":\"#e8e8e8\",\"colorBorder\":\"#ccc\",\"majorTickFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ccc\"},\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"animationTarget\":\"needle\",\"majorTicks\":[\"N\",\"NE\",\"E\",\"SE\",\"S\",\"SW\",\"W\",\"NW\"]},\"title\":\"Compass\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "temperature_gauge_canvas_gauges", - "name": "Thermometer scale", - "image": "data:image/png;base64,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", - "description": "Preconfigured widget to display temperature. Allows to configure temperature range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 7, - "sizeY": 3, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueLinearGauge(self.ctx, 'linearGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-analogue-linear-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 30 - 15;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"defaultColor\":\"#e64a19\",\"barStrokeWidth\":2.5,\"colorBar\":\"rgba(255, 255, 255, 0.4)\",\"colorBarEnd\":\"rgba(221, 221, 221, 0.38)\",\"showUnitTitle\":true,\"minorTicks\":2,\"valueBox\":true,\"valueInt\":3,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"colorNeedleShadowUp\":\"rgba(2,255,255,0.2)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"highlightsWidth\":10,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"showBorder\":false,\"majorTicksCount\":8,\"numbersFont\":{\"family\":\"Arial\",\"size\":18,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#78909c\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":26,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#37474f\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":40,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#444\",\"shadowColor\":\"rgba(0,0,0,0.3)\"},\"minValue\":-60,\"highlights\":[{\"from\":-60,\"to\":-40,\"color\":\"#90caf9\"},{\"from\":-40,\"to\":-20,\"color\":\"rgba(144, 202, 249, 0.66)\"},{\"from\":-20,\"to\":0,\"color\":\"rgba(144, 202, 249, 0.33)\"},{\"from\":0,\"to\":20,\"color\":\"rgba(244, 67, 54, 0.2)\"},{\"from\":20,\"to\":40,\"color\":\"rgba(244, 67, 54, 0.4)\"},{\"from\":40,\"to\":60,\"color\":\"rgba(244, 67, 54, 0.6)\"},{\"from\":60,\"to\":80,\"color\":\"rgba(244, 67, 54, 0.8)\"},{\"from\":80,\"to\":100,\"color\":\"#f44336\"}],\"unitTitle\":\"Temperature\",\"units\":\"°C\",\"colorBarProgress\":\"#90caf9\",\"colorBarProgressEnd\":\"#f44336\",\"colorBarStroke\":\"#b0bec5\",\"valueDec\":1},\"title\":\"Thermometer scale\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "temperature_radial_gauge_canvas_gauges", - "name": "Temperature radial gauge", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display temperature. Allows to configure temperature range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 6, - "sizeY": 5, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-analogue-radial-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":60,\"startAngle\":67.5,\"ticksAngle\":225,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":-60,\"to\":-50,\"color\":\"#42a5f5\"},{\"from\":-50,\"to\":-40,\"color\":\"rgba(66, 165, 245, 0.83)\"},{\"from\":-40,\"to\":-30,\"color\":\"rgba(66, 165, 245, 0.66)\"},{\"from\":-30,\"to\":-20,\"color\":\"rgba(66, 165, 245, 0.5)\"},{\"from\":-20,\"to\":-10,\"color\":\"rgba(66, 165, 245, 0.33)\"},{\"from\":-10,\"to\":0,\"color\":\"rgba(66, 165, 245, 0.16)\"},{\"from\":0,\"to\":10,\"color\":\"rgba(229, 115, 115, 0.16)\"},{\"from\":10,\"to\":20,\"color\":\"rgba(229, 115, 115, 0.33)\"},{\"from\":20,\"to\":30,\"color\":\"rgba(229, 115, 115, 0.5)\"},{\"from\":30,\"to\":40,\"color\":\"rgba(229, 115, 115, 0.66)\"},{\"from\":40,\"to\":50,\"color\":\"rgba(229, 115, 115, 0.83)\"},{\"from\":50,\"to\":60,\"color\":\"#e57373\"}],\"showUnitTitle\":true,\"colorPlate\":\"#cfd8dc\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"valueDec\":1,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"°C\",\"majorTicksCount\":12,\"numbersFont\":{\"family\":\"Roboto\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"unitTitle\":\"Temperature\",\"minValue\":-60},\"title\":\"Temperature radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "speed_gauge_canvas_gauges", - "name": "Speed gauge", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display speed. Allows to configure speed range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 7, - "sizeY": 5, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-analogue-radial-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "radial_gauge_canvas_gauges", - "name": "Radial gauge", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display any value reading. Allows to configure value range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 6, - "sizeY": 5, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-analogue-radial-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < -100) {\\n\\tvalue = -100;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":10,\"highlights\":[],\"showUnitTitle\":true,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":10,\"valueInt\":3,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":36,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"minValue\":-100,\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - } + "widgetTypeFqns": [ + "analogue_gauges.temperature_radial_gauge_canvas_gauges", + "analogue_gauges.temperature_gauge_canvas_gauges", + "analogue_gauges.speed_gauge_canvas_gauges", + "analogue_gauges.radial_gauge_canvas_gauges", + "analogue_gauges.analogue_compass" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 473d3d215b..8c92636cb2 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -2,229 +2,22 @@ "widgetsBundle": { "alias": "cards", "title": "Cards", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAT5UlEQVR42u3d95MUVffHcf86/UEtqwxVapmzaH0VE2YR82PAgBkTKCYUAyrGR0mSxERwBXRV0EdFRcAc5vvaPnptZ5bdZWbDwJ5TW1s93T23b5/7vufe7t3PuXv8+OOP36YNZrzUqCzdNUR37fHdd9810gaz4qV01xDdlWAlWAlWgpVgJVgJVnoqwUqwEqwEK8FKsNISrAQrwdqlwfrtt9/+V7Off/55gIJ++OGH8m46bOvWrb/88sv4Aeubb76pu8vb5z/++MPvEaqPwocL8RUrVqhwfc8rr7zSYdsNBFZvb+/hhx/+n79t/fr1ree8+eabn376qY03KrPx2muvff311zaeffbZd955Z/yANWvWLF76v8psPPLII19++eVdd901QvX57LPP7rvvvqGfv3nz5ldffbXfQzfffPPy5cvre9zC999/P4JgnXfeefU9n3/+uX6pEkHMxo0bL7vsMh5UCU786quvfGXSpElz584VwByNkvUtX3nrrbdsRDmrVq1auHDhtm3bdr+h8LHHHpszZ05s//TTT9EbN2zYwG+LFy/mH+OACMGBf/75Z5zW09Pj0Pbt2wsxuuimTZvi46+//qr3vvvuu3H+hx9++MUXX/g6D3/88cdxzkcffbRo0aItW7bER+FHCa5VamW0ef75588555z4iootW7Zs5cqV0SLA0hyuUmJHAUutHOo3pnQElqpsq8xt2DNjxowzzjjjySefPP/8819++eUPPvjAxh133AGpJ5544plnnnHDp512mtNUa/r06QsWLOCOKVOmPPTQQ7rXDTfcoJCHH374+uuvd/KECROaRs/dDCwOvPjii21Mnjz58ssvF8JPPvnkK664wsYll1zCAw5xpvD21FNPcazRR5fj8xdeeIEbuff333/nYWXedttt/On8s88++8ILL5w3b56jV155ZQxbPKxMNJh+QEdRgVGJQ1pw5syZTnj77beRfdZZZ6nk7bffrtgAK/b4CowKWO5UUUoWPlxlOME69NBDL67MtQOsl156yYYa33rrrTbUbMmSJTYCLBvuVhe0EWDpYVwT0w5u1VduvPFG96AP6cfjByxBJToVkmysXr0aTwLGUUcdJbRzztSpU5cuXWq0uvba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", - "description": "Tables and cards to display latest and historical values for multiple entities simultaneously.", + "image": "data:image/png;base64,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", + "description": "Cards to display latest and historical values for multiple entities simultaneously.", "externalId": null, "name": "Cards" }, - "widgetTypes": [ - { - "alias": "attributes_card", - "name": "Attributes card", - "image": "data:image/png;base64,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", - "description": "Displays one or more latest values of the entity. Supports multiple entities.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "", - "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", - "controllerScript": "self.onInit = function() {\n \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n \n for (var i=0; i < self.ctx.datasources.length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Attributes card\"}" - } - }, - { - "alias": "html_card", - "name": "HTML Card", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAATFSURBVHja7d3tUxNXFMdx/u7vEkAJsKQK6bilKaBV1Eh5GGNaSyq0WodhRuqMtQJqW5UHCwKGMRJiQpL99cWGEGaaTjtjgaTnvNpz7mYnn8neu/duXtwWFbdWlxs8Vrf21VJcy5TU4FHKrBVbtjJqgshstayWmgFSWm1ZVlPEskEMYhCDGMQgBjGIQQxiEIMYxCAGMYhBDGIQgxjEIAYxiEEMYhCDGMQgBjGIQQxiEIMYxCAG+T9ACrlcLjjyc7lcWflcbfhBfnByOZfL5cpSKZfL+f/2G+WAJ/8dJAHh4GgFeK4BaiOjMYDtyskzAIvSQ+BtI0JmKif3NRBk3PM8Lwwhz/M8LxtAzgf30RoNBJEkjUOk0j4GwJok6XaDQ8LwtSSVXVrP1of4v1w978bu5YNh4fHlfnfwfpDo15Hevm/eHkCyP8Tc/rGV44d8FaLbl/QCRrrqQopXg251bkdSdjBIIpuSNAtAbwXyqgsA595Hh3Sm0+l0Or1QDzJxLWiYhCftdSFJoKMHiPnyvwTOhIHeD9KGA3SfIYBkOsGJtIPz/GNDauIvIfEFGJdKnZwtOPUgOw4kfT0E1vQMmPb12IEZaRJCz1RKBpBJCG+ocAVixw25XArTUdACTBbqdvY56MhLisK8xisDXRyiUi+MStoDnqjUCT9Keg3Oh48MCcXj8Xg8PlwPMqQE/Kzr8HK3LiQBFyozhaI8uClJ8+AUi8CD6qi1DYymUqkksH7Mnf0zLcNIvo2In64LGYXhauLCHUlaAnb3gIUq5FXN7//qmCH98s8RmoXv9KYuZBI+ryZ9cFsKzsvngUdVyAowcDGIjWOGRKRpaINNrdWF3IUeX9LS/PymLsMlSZqCLikM01XIe+CnE3ogRqRtAE9arQtZCVqynbCoWXBWpfddMCaNgLsnLQSj1gVwM5Je3/RPAKIBYPYIJOp5nud5dw8uFIPQxO1eCOf1oRs6Jm71QOtG0FMiydFQAHkKnJlIjTgkTwIyB87uEUglbh5c6G13UGhdkvSyPUicB5J0HYBQe/Bkv+tUPnr/JCDZVq7obyF6d6MNnC+COdTmSCs4A78Hs5dkG3zyYqAy13oec8CJ/XZ6l7qFzfW9apJ/80dNsr5V2yNyGxt7tmY3iEEMYhCDGOTEIJnD6YtBDGKQpocsDrvR5K4kyX96PeoOzu5Lysbj8b2Zvv7SkepphgwBcD4vqRAPVlXR91IauASUVAhWgny6e7ohtEdCwcpdCaDTBYYDCED5sHrxdEOuFZRx4Ya06cC3ZS05sKI04EzM3demA1NlLTqweuo7ewI86U7lHdYQzCgNzEnSHXB9SYPw/amHpKBPGoFzqVQqFYUxpYEX0pHqeINAav4wvXoI+eyweq1BIDHorry+nT6E1FRnGgQyCtFqUxVyo7baGJBHlVG4mFivgRxU9xMbjQIpR4HBqYRLz84hpBwFhqYSLu5Og0C07VZ6dXTvEHKk2iAQZZOdQHcqX3NrSdlb1WrjLHXL26/T/j+s2prdIAYxiEEMYhCDGMQgBjGIQQxiEIMYxCAGMYhBDGIQgxjEIAYxiEEMYhCDGMQgBjGIQQxiEIMY5PRAmmaD4ObYsvndVst+c2yiXWppjm3NS/oTe0OjFEeU1MMAAAAASUVORK5CYII=", - "description": "Useful to inject custom HTML code. Designed to display static information only.", - "descriptor": { - "type": "static", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n var $injector = self.ctx.$scope.$injector;\n var utils = $injector.get(self.ctx.servicesMap.get('utils'));\n\n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n var evtFnPrefix = 'htmlCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml + self.ctx.widget.id));\n cardHtml = '
' + \n self.ctx.settings.cardHtml + \n '
';\n cardHtml = replaceCustomTranslations(cardHtml);\n self.ctx.$container.html(cardHtml);\n\n window[evtFnPrefix + '_onClickFn'] = function (event) {\n self.ctx.actionsApi.elementClick(event);\n }\n\n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function replaceCustomTranslations (pattern) {\n var customTranslationRegex = new RegExp('{i18n:[^{}]+}', 'g');\n pattern = pattern.replace(customTranslationRegex, getTranslationText);\n return pattern;\n }\n \n function getTranslationText (variable) {\n return utils.customTranslation(variable, variable);\n \n }\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-html-card-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}" - } - }, - { - "alias": "timeseries_table", - "name": "Timeseries table", - "image": "data:image/png;base64,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", - "description": "Displays time series data for one or more entities. Data for each entity is displayed in a separate tab.", - "descriptor": { - "type": "timeseries", - "sizeX": 8, - "sizeY": 6.5, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "latestDataKeySettingsSchema": "", - "settingsDirective": "tb-timeseries-table-widget-settings", - "dataKeySettingsDirective": "tb-timeseries-table-key-settings", - "latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings", - "hasBasicMode": true, - "basicModeDirective": "tb-timeseries-table-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}" - } - }, - { - "alias": "html_value_card", - "name": "HTML Value Card", - "image": "data:image/png;base64,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", - "description": "Displays configurable HTML with ability to inject values from the selected datasource. For example, display single or multiple attribute values.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n self.ctx.htmlSet = false;\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-value-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n var evtFnPrefix = 'htmlValueCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml + self.ctx.widget.id));\n self.ctx.html = '
' + \n self.ctx.settings.cardHtml + \n '
';\n\n self.ctx.replaceInfo = processHtmlPattern(self.ctx.html, self.ctx.data);\n \n updateHtml();\n \n window[evtFnPrefix + '_onClickFn'] = function (event) {\n self.ctx.actionsApi.elementClick(event);\n }\n\n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function processHtmlPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n if (label == 'entityName') {\n variableInfo.isEntityName = true;\n } else if (label == 'entityLabel') {\n variableInfo.isEntityLabel = true;\n } else if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (!variableInfo.isEntityName && !variableInfo.isEntityLabel && variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n}\n\nself.onDataUpdated = function() {\n updateHtml();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n singleEntity: true,\n dataKeysOptional: true\n };\n}\n\n\nself.onDestroy = function() {\n}\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateHtml() {\n var $injector = self.ctx.$scope.$injector;\n var utils = $injector.get(self.ctx.servicesMap.get('utils'));\n var text = self.ctx.html;\n var updated = false;\n for (var v in self.ctx.replaceInfo.variables) {\n var variableInfo = self.ctx.replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n } else if (variableInfo.isEntityName) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n } else if (variableInfo.isEntityLabel) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityLabel || self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n }\n if (typeof variableInfo.lastVal === undefined ||\n variableInfo.lastVal !== txtVal) {\n updated = true;\n variableInfo.lastVal = txtVal;\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !self.ctx.htmlSet) {\n text = replaceCustomTranslations(text);\n self.ctx.$container.html(text);\n if (!self.ctx.htmlSet) {\n self.ctx.htmlSet = true;\n }\n }\n \n function replaceCustomTranslations (pattern) {\n var customTranslationRegex = new RegExp('{i18n:[^{}]+}', 'g');\n pattern = pattern.replace(customTranslationRegex, getTranslationText);\n return pattern;\n }\n \n function getTranslationText (variable) {\n return utils.customTranslation(variable, variable);\n \n }\n}\n\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-html-card-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"My value\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.random() * 5.45;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"cardCss\":\".card {\\n width: 100%;\\n height: 100%;\\n border: 2px solid #ccc;\\n box-sizing: border-box;\\n}\\n\\n.card .content {\\n padding: 20px;\\n display: flex;\\n flex-direction: row;\\n align-items: center;\\n justify-content: space-around;\\n height: 100%;\\n box-sizing: border-box;\\n}\\n\\n.card .content .column {\\n display: flex;\\n flex-direction: column; \\n justify-content: space-around;\\n height: 100%;\\n}\\n\\n.card h1 {\\n text-transform: uppercase;\\n color: #999;\\n font-size: 20px;\\n font-weight: bold;\\n margin: 0;\\n padding-bottom: 10px;\\n line-height: 32px;\\n}\\n\\n.card .value {\\n font-size: 38px;\\n font-weight: 200;\\n}\\n\\n.card .description {\\n font-size: 20px;\\n color: #999;\\n}\\n\",\"cardHtml\":\"
\\n
\\n
\\n

Value title

\\n
\\n ${My value:2} units.\\n
\\n
\\n Value description text\\n
\\n
\\n \\n
\\n
\"},\"title\":\"HTML Value Card\",\"dropShadow\":false,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "simple_card", - "name": "Simple card", - "image": "data:image/png;base64,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", - "description": "Designed to display single value of the selected attribute or timeseries data. Widget styles are customizable.", - "descriptor": { - "type": "latest", - "sizeX": 5, - "sizeY": 3, - "resources": [], - "templateHtml": "", - "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.tbDatasource-table {\n width: 100%;\n height: 100%;\n border-collapse: collapse;\n white-space: nowrap;\n font-weight: 100;\n text-align: right;\n}\n\n.tbDatasource-table td {\n padding: 12px;\n position: relative;\n box-sizing: border-box;\n}\n\n.tbDatasource-data-key {\n opacity: 0.7;\n font-weight: 400;\n font-size: 3.500rem;\n}\n\n.tbDatasource-value {\n font-size: 5.000rem;\n}", - "controllerScript": "self.onInit = function() {\n\n self.ctx.labelPosition = self.ctx.settings.labelPosition || 'left';\n \n if (self.ctx.datasources.length > 0) {\n var tbDatasource = self.ctx.datasources[0];\n var datasourceId = 'tbDatasource' + 0;\n self.ctx.$container.append(\n \"
\"\n );\n \n self.ctx.datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n \n var tableId = 'table' + 0;\n self.ctx.datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n if (self.ctx.labelPosition === 'top') {\n table.css('text-align', 'left');\n }\n \n if (tbDatasource.dataKeys.length > 0) {\n var dataKey = tbDatasource.dataKeys[0];\n var labelCellId = 'labelCell' + 0;\n var cellId = 'cell' + 0;\n if (self.ctx.labelPosition === 'left') {\n table.append(\n \"\" +\n dataKey.label +\n \"\");\n } else {\n table.append(\n \"\" +\n dataKey.label +\n \"\");\n }\n self.ctx.labelCell = $('#' + labelCellId, table);\n self.ctx.valueCell = $('#' + cellId, table);\n self.ctx.valueCell.html(0 + ' ' + self.ctx.units);\n }\n }\n \n $.fn.textWidth = function(){\n var html_org = $(this).html();\n var html_calc = '' + html_org + '';\n $(this).html(html_calc);\n var width = $(this).find('span:first').width();\n $(this).html(html_org);\n return width;\n }; \n \n self.onResize();\n};\n\nself.onDataUpdated = function() {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n\n if (self.ctx.valueCell && self.ctx.data.length > 0) {\n var cellData = self.ctx.data[0];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var txtValue;\n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (self.ctx.datasources.length > 0 && self.ctx.datasources[0].dataKeys.length > 0) {\n dataKey = self.ctx.datasources[0].dataKeys[0];\n if (dataKey.decimals || dataKey.decimals === 0) {\n decimals = dataKey.decimals;\n }\n if (dataKey.units) {\n units = dataKey.units;\n }\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCell.html(txtValue);\n var targetWidth;\n var minDelta;\n if (self.ctx.labelPosition === 'left') {\n targetWidth = self.ctx.datasourceContainer.width() - self.ctx.labelCell.width();\n minDelta = self.ctx.width/16 + self.ctx.padding;\n } else {\n targetWidth = self.ctx.datasourceContainer.width();\n minDelta = self.ctx.padding;\n }\n var delta = targetWidth - self.ctx.valueCell.textWidth();\n var fontSize = self.ctx.valueFontSize;\n if (targetWidth > minDelta) {\n while (delta < minDelta && fontSize > 6) {\n fontSize--;\n self.ctx.valueCell.css('font-size', fontSize+'px');\n delta = targetWidth - self.ctx.valueCell.textWidth();\n }\n }\n }\n } \n \n};\n\nself.onResize = function() {\n var labelFontSize;\n if (self.ctx.labelPosition === 'top') {\n self.ctx.padding = self.ctx.height/20;\n labelFontSize = self.ctx.height/4;\n self.ctx.valueFontSize = self.ctx.height/2;\n } else {\n self.ctx.padding = self.ctx.width/50;\n labelFontSize = self.ctx.height/2.5;\n self.ctx.valueFontSize = self.ctx.height/2;\n if (self.ctx.width/self.ctx.height <= 2.7) {\n labelFontSize = self.ctx.width/7;\n self.ctx.valueFontSize = self.ctx.width/6;\n }\n }\n self.ctx.padding = Math.min(12, self.ctx.padding);\n \n if (self.ctx.labelCell) {\n self.ctx.labelCell.css('font-size', labelFontSize+'px');\n self.ctx.labelCell.css('padding', self.ctx.padding+'px');\n }\n if (self.ctx.valueCell) {\n self.ctx.valueCell.css('font-size', self.ctx.valueFontSize+'px');\n self.ctx.valueCell.css('padding', self.ctx.padding+'px');\n } \n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n};\n\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-simple-card-widget-settings", - "hasBasicMode": true, - "basicModeDirective": "tb-simple-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true}" - } - }, - { - "alias": "label_widget", - "name": "Label widget", - "image": "data:image/png;base64,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", - "description": "Displays static image and multiple values of selected attributes or timeseries keys on top of it. Position of the values on the image is configurable using advanced settings.", - "descriptor": { - "type": "latest", - "sizeX": 4.5, - "sizeY": 5, - "resources": [], - "templateHtml": "", - "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n \n var imageUrl = self.ctx.settings.backgroundImageUrl ? self.ctx.settings.backgroundImageUrl :\n 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==';\n\n self.ctx.$container.css('background', 'url(\"'+imageUrl+'\") no-repeat');\n self.ctx.$container.css('backgroundSize', 'contain');\n self.ctx.$container.css('backgroundPosition', '50% 50%');\n \n function processLabelPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n var configuredLabels = self.ctx.settings.labels;\n if (!configuredLabels) {\n configuredLabels = [];\n }\n \n self.ctx.labels = [];\n\n for (var l = 0; l < configuredLabels.length; l++) {\n var labelConfig = configuredLabels[l];\n var localConfig = {};\n localConfig.font = {};\n \n localConfig.pattern = labelConfig.pattern ? labelConfig.pattern : '${#0}';\n localConfig.x = labelConfig.x ? labelConfig.x : 0;\n localConfig.y = labelConfig.y ? labelConfig.y : 0;\n localConfig.backgroundColor = labelConfig.backgroundColor ? labelConfig.backgroundColor : 'rgba(0,0,0,0)';\n \n var settingsFont = labelConfig.font;\n if (!settingsFont) {\n settingsFont = {};\n }\n \n localConfig.font.family = settingsFont.family || 'Roboto';\n localConfig.font.size = settingsFont.size ? settingsFont.size : 6;\n localConfig.font.style = settingsFont.style ? settingsFont.style : 'normal';\n localConfig.font.weight = settingsFont.weight ? settingsFont.weight : '500';\n localConfig.font.color = settingsFont.color ? settingsFont.color : '#fff';\n \n localConfig.replaceInfo = processLabelPattern(localConfig.pattern, self.ctx.data);\n \n var label = {};\n var labelElement = $('
');\n labelElement.css('position', 'absolute');\n labelElement.css('display', 'none');\n labelElement.css('top', '0');\n labelElement.css('left', '0');\n labelElement.css('backgroundColor', localConfig.backgroundColor);\n labelElement.css('color', localConfig.font.color);\n labelElement.css('fontFamily', localConfig.font.family);\n labelElement.css('fontStyle', localConfig.font.style);\n labelElement.css('fontWeight', localConfig.font.weight);\n \n labelElement.html(localConfig.pattern);\n self.ctx.$container.append(labelElement);\n label.element = labelElement;\n label.config = localConfig;\n label.htmlSet = false;\n label.visible = false;\n self.ctx.labels.push(label);\n }\n\n var bgImg = $('');\n bgImg.hide();\n bgImg.bind('load', function()\n {\n self.ctx.bImageHeight = $(this).height();\n self.ctx.bImageWidth = $(this).width();\n self.onResize();\n });\n self.ctx.$container.append(bgImg);\n bgImg.attr('src', imageUrl);\n \n self.onDataUpdated();\n}\n\nself.onDataUpdated = function() {\n updateLabels();\n}\n\nself.onResize = function() {\n if (self.ctx.bImageHeight && self.ctx.bImageWidth) {\n var backgroundRect = {};\n var imageRatio = self.ctx.bImageWidth / self.ctx.bImageHeight;\n var componentRatio = self.ctx.width / self.ctx.height;\n if (componentRatio >= imageRatio) {\n backgroundRect.top = 0;\n backgroundRect.bottom = 1.0;\n backgroundRect.xRatio = imageRatio / componentRatio;\n backgroundRect.yRatio = 1;\n var offset = (1 - backgroundRect.xRatio) / 2;\n backgroundRect.left = offset;\n backgroundRect.right = 1 - offset;\n } else {\n backgroundRect.left = 0;\n backgroundRect.right = 1.0;\n backgroundRect.xRatio = 1;\n backgroundRect.yRatio = componentRatio / imageRatio;\n var offset = (1 - backgroundRect.yRatio) / 2;\n backgroundRect.top = offset;\n backgroundRect.bottom = 1 - offset;\n }\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var labelLeft = backgroundRect.left*100 + (label.config.x*backgroundRect.xRatio);\n var labelTop = backgroundRect.top*100 + (label.config.y*backgroundRect.yRatio);\n var fontSize = self.ctx.height * backgroundRect.yRatio * label.config.font.size / 100;\n label.element.css('top', labelTop + '%');\n label.element.css('left', labelLeft + '%');\n label.element.css('fontSize', fontSize + 'px');\n if (!label.visible) {\n label.element.css('display', 'block');\n label.visible = true;\n }\n }\n } \n}\n\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateLabels() {\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var text = label.config.pattern;\n var replaceInfo = label.config.replaceInfo;\n var updated = false;\n for (var v = 0; v < replaceInfo.variables.length; v++) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n updated = true;\n } else {\n txtVal = val;\n updated = true;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !label.htmlSet) {\n label.element.html(text);\n if (!label.htmlSet) {\n label.htmlSet = true;\n }\n }\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n singleEntity: true\n };\n};\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-label-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"var\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"backgroundImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"labels\":[{\"pattern\":\"Value: ${#0:2} units.\",\"x\":20,\"y\":47,\"font\":{\"color\":\"#515151\",\"family\":\"Roboto\",\"size\":6,\"style\":\"normal\",\"weight\":\"500\"}}]},\"title\":\"Label widget\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "entities_table", - "name": "Entities table", - "image": "data:image/png;base64,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", - "description": "Displays list of entities that match selected alias and filter with ability of additional full text search and pagination. Highly customizable using widget styles, data source keys and widget actions.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 6.5, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-entities-table-widget-settings", - "dataKeySettingsDirective": "tb-entities-table-key-settings", - "hasBasicMode": true, - "basicModeDirective": "tb-entities-table-basic-config", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}" - } - }, - { - "alias": "entities_hierarchy", - "name": "Entities hierarchy", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAr9SURBVHja7d17VFTHGQDwbx+siDxUFMEgKCm2JsZgFmNUVDAoRzERQfRgfTTa2Go18XlQo1WDmlVaS6I2XWuJgHIEHynVoMYiGuN7j5zjM0qQtw9wBURQll2+zty5CwtiT633biuZ+eNymbv38WNm7t35mNkFrC8rKnjJU1GZCaG+uMqML3kyVxXXQ1kVtoFUVQZF5rYAMRdBAbaJVMAhHMIhHMIhHMIhHMIhdoXc4BAO4RAOsU33/rI6UaJ73s4zEkNuNKUWfeKcuy13/cqxW7CPw7bWjro16/mu4ir0kxryjGJ5FAialOZ7nlHGWtA8R3WllaN6L3y+q1jsBQb7QLYBwCtoWffe/sas8b4msnzoEIffJZCVXN0DxOJ18zY9QIPObaiuAvHU0rk76xFv60qT568pbUibt/I63fH6yiXpJHtf2sl559ihTN02aufYE7IbwKHEmuXKTn3qMq7oTH5mQC4aOrwzw9ezKDO6fe/ou7hKETbRedhjPAe9Qie6+ET3ifF1JEdN07wb4xTRgBGejv32sENlqEoSOj22T9UaAJqduJVwrlpzYL11YyNkal8LGt0+YVXrnOILxBzNGgLZiJgNg+uxwmUFlrsuQzwOhzHC5Zr1AONC8Z46VWrIEpoam31TY7+HWB3musSaYYQ/PQWZ437Y3NhG5ruT+oOTBhDIScTHVINvTsNERTlZ6bEGI4ZZ97/rkIQYHmqXEsGiM1drm2U8ht8/BTFGa7r+5qwIGf823bbajUHqBfdbU3AFCGmmDSRefdxgWKUskB+SP6cHQEhZQ86XuuQia2aPSeyGk9IEQaxIDVfsZZBfC3fUBV1bQDZozhtIyreBvMZsq2WHXHElp3E2XtIK55tUyXLnOd4RngCJGK8hFSqdQOZmkJx+EwhkAeLnmmLEhn5jW0C+ge9peWIT5AxpMCR90NMiN2Qtvf4gHMP+cEJlp891r8Dvbx99w6+aNOS4kmO9CSTc51RVlmscYkBQ0ZMH3YMuF3+s/K4FxPTGq8cqD3fPaoLM8hIaVjb8U+4n+yp6+cEYLEJWidm5I5WgmUSr2gIldP2QQMrHq8BhZh1ikhou49WBAL7kudMcgiVhSnBebmmE1LgtEH5afH8p93utvKMkGdBwlKW8xg2VhfVspbZUzKnOf8K23BEKraShtXM+yK/j7345hEM4hEN+epAbHMIhHMIhTelbvX5HVk1rW2oT7rW6R5X+Jn2bqadbc/UVVXpr732P/j7bUX+avnXV03SenmLDH85KCHkSqgCbFCxmR2r8fJRum1qLwin3t3rceuflZLkbtpLlSqe6PFAIUSMsVEGOsLIE5pOlXtGJpLVoGt1+1GBFrHSQUxATa5P0Vgjp4T7a4rChlQM8ekYhjhpOFr9SjCPLd0dgHihZB2WlkkEM7RwpZJW/2MN3vIQYp/hBMkg2ZLf2qkihq77SkYTrMGvRquOkl0ULolRXeF9HejINBxYuFfYr/Wx+olgD45xM2OAZ5mJCs/NqAhnhTmNc9a+EChDTmx97U8iHIezV4aNoFYT9ckDm0rrlessGkgvJiIs00eGKeFzuQbqy8S411+EbxCntIserSOTrdEfttK79meQEnMWLYFCfIMtjBJKo3kly/65IESBrfasFSPjkQ9tPWrv1WXBdDkiwT2xsjPgrg9SrV2OW4iDiunb3r5Grw0HTkUK+hiOIOxR5lp9FWTDfKYFFlhw34fpf4JDluEVTSyD/GEd7wWNGZlPIdcdMFCD9FW6dYDjrW9YFjnmRNtK8Q28LCW76lUHQZQnO6CMcPwsDZ2OJ4lsBMqUvvXDdzdNCLGVkDNt/aBQOXYRxgTh5MLk7QXomXMMC5b7DBGIJIj15AZL8NzMmilHB37kXoMQlki2EJZ6CVJJYSwi7nX2FCZ7mzd3NAiQknO28k20bwn77xLNCnYUXlGW9llJIiqXXfFzhZcogkC1dykQII48S2nu74yg1ZKuqY8+apyHHSGzn/UAaiTOU41318RASsaOQqBFi2Boy6Daxoh+BTa4mtHgkQCaFJOH6ztXdV5B6mFPj4q7VajUeWjEuOC2ALFLU+1ByiPfXdYHbn4I8CfI347JOFSwUh2Mmqi4yyCfupH0boy/mwXZxG00P1X1IVA+nvq6qYpC7mgmqQgqpS6fJfUx69WWg/4PQRhK25q8oC0TbHOJvyN7SvzN5At9yGXnlwVYPUjVS4TVkkLwOkwsKIj0fYoT7PuP5178UjzWAxCsRd4EWGQQnwlikELaZVq2GwIFXyj6FQ3jBeTItzEsSPhBn6XSFtGrVBvvpdLOsEAB1z9n5wiv6AnTfRWNwLutFCGb7Awy4TAphmgNoPrA+IhcpaRysXHgSCpAsONACgoVhAB3jET9izctHMkjdKCVAttDYbZ8jzdLt4qfjuSXiPyOr82qf833hoxL+7pdDOIRDOIRDOIRDOOSnGqAjoTcacrEkpWKBvkzMSxPCccLqnRPWF2bp5YBIFqDDNGHI0UcOR0jH8YKY19WJhuPIWfaOVU8U83LbgxwQ6QJ0AkSnSMYmSJ0ija2Md57eU4Q0hHSQBSJdgI5CkhSfoQ2kAE6xlbOP8G0Rsq1LrNyQFwzQEcghh7loCzkNx3fsucPWRUipW0q83JAXDNClwQ7ndyzNIHvBwUvV/qAtZFwYSgWRK0CXBu06O5xuBvlxTT7eecv7cRMkrUO+VBCUK0CXBsOMAT3KbSFiuOt8I8TYjdwI5YRIEaBLg0uY6zba0hJyAw42QhZBgFbrDdoNckGkCNAJt999irU2kIj3yGI/XGuEnKOBuqmQflFGyAsH6NgDcYHqGIGk0IIqxP1K/f1zvYMabBo7ylW1pAvQMYhpkOftDNZ0fou4uRMoh+WjPSAyB+gaip8vfscH1XAIh3AIh3AIh3AIh/x/QiSa4vqEROISD9+VFvK/mOJqBHc/vw6Of5QW8oxikXOKqxE+J52Cucof7QERp7jGjW4aziLZFFcBgjmwz36QXaT31zh9T7IprgzyZ8Ulu1QtYYrrF7JMcTVCaGxslFO81G3k30xxrQp1WdRUs6Wa4mqEgOjo4c6LLfYoETmnuLKqdUqZKD9E3imuDII9pssOkXmKK4NUOS2THSLzFFcjLDZc2B/kdF32J7u8U1wJhCSPsLPSv9dqmV7aKa4vZeIQDuEQDmmzkBscwiEcwiFN6b8aQafXJ5+gHdCbev22AzaTK2p3rdlmjaf9oC+RByLlCDpvPx+NJxnxlwQ9/NqrYqwfyV/k5/X+z93Y5+yZ+sFReSASj6C7P/BVCslD8wnfoeJn8kf1qUZzyCBhPa6DXBBpR9DRYQ1VAoRMzlWI86eX7yaLdU509ZpjvB0gLz6Cju7oZREh2DfS5jxRdMaeZcj0HDtAXngE3czYhUHdjqEVEhFgPXLm5kh32gfe7HFfQoh8I+jCoiMDPFIbIVP9MYXcyGhZ93MKIRdY6JKK0kGwtRKRZgQdrVpx6jwrJHgITgoNncA6+MNIHHZ0OMoNkWYEHYXcgBQRYuoy1+Y86XArE/y02j7gHyUjRLIRdJhJ7AyyTsXGyTV0op+hkKioLKDD5zbCykPyQiQYQbfHcGa7d9/HBJJxNnmywhqYn9XxYMUZf7E2yla1pBxBB6DqOaOUNnaAbuGNF1wzWwPqmHKZIXaZ4lpf9J/G8nifnUM4hEM4hEM4hEM4hEM4hEM4hEM4hENkhrSZLwhuG1/ZXFkGprbxJdpmaBtfa27GfwEB0j8MtCzTjwAAAABJRU5ErkJggg==", - "description": "Displays hierarchy of entities based on their relations. The root of the hierarchy is defined using entity alias. By default, displays entities related using \"Contains\" relation. You may change the behaviour using advanced settings.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3.5, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesHierarchyWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-entities-hierarchy-widget-settings", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"var entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: \\\"FROM\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n/**\\n\\n// Function should return relations query object for current node used to fetch entity children.\\n// Function can return 'default' string value. In this case default relations query will be used.\\n\\n// The following example code will construct simple relations query that will fetch relations of type 'Contains'\\n// from the current entity.\\n\\nvar entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: \\\"FROM\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n**/\\n\",\"nodeHasChildrenFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node has children (whether it can be expanded).\\n\\n// The following example code will restrict entities hierarchy expansion up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n// The next example code will restrict entities expansion according to the value of example 'nodeHasChildren' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) {\\n return data['nodeHasChildren'] === 'true';\\n} else {\\n return true;\\n}\\n \\n**/\\n \",\"nodeOpenedFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be opened (expanded) when it first loaded.\\n\\n// The following example code will open by default nodes up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n**/\\n \",\"nodeDisabledFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be disabled (not selectable).\\n\\n// The following example code will disable current node according to the value of example 'nodeDisabled' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) {\\n return data['nodeDisabled'] === 'true';\\n} else {\\n return false;\\n}\\n \\n**/\\n\",\"nodeIconFunction\":\"/** \\n\\n// Function should return node icon info object.\\n// Resulting object should contain either 'materialIcon' or 'iconUrl' property. \\n// Where:\\n - 'materialIcon' - name of the material icon to be used from the Material Icons Library (https://material.io/tools/icons);\\n - 'iconUrl' - url of the external image to be used as node icon.\\n// Function can return 'default' string value. In this case default icons according to entity type will be used.\\n\\n// The following example code shows how to use external image for devices which name starts with 'Test' and use \\n// default icons for the rest of entities.\\n\\nvar entity = nodeCtx.entity;\\nif (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) {\\n return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'};\\n} else {\\n return 'default';\\n}\\n \\n**/\",\"nodeTextFunction\":\"/**\\n\\n// Function should return text (can be HTML code) for the current node.\\n\\n// The following example code will generate node text consisting of entity name and temperature if temperature value is present in entity attributes/timeseries.\\n\\nvar data = nodeCtx.data;\\nvar entity = nodeCtx.entity;\\nvar text = entity.name;\\nif (data.hasOwnProperty('temperature') && data['temperature'] !== null) {\\n text += \\\" \\\"+ data['temperature'] +\\\" °C\\\";\\n}\\nreturn text;\\n\\n**/\",\"nodesSortFunction\":\"/**\\n\\n// This function is used to sort nodes of the same level. Function should compare two nodes and return \\n// integer value: \\n// - less than 0 - sort nodeCtx1 to an index lower than nodeCtx2\\n// - 0 - leave nodeCtx1 and nodeCtx2 unchanged with respect to each other\\n// - greater than 0 - sort nodeCtx2 to an index lower than nodeCtx1\\n\\n// The following example code will sort entities first by entity type in alphabetical order then\\n// by entity name in alphabetical order.\\n\\nvar result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);\\nif (result === 0) {\\n result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);\\n}\\nreturn result;\\n \\n**/\"},\"title\":\"Entities hierarchy\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"widgetStyle\":{},\"actions\":{}}" - } - }, - { - "alias": "qr_code", - "name": "QR Code", - "image": "data:image/png;base64,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", - "description": "Displays QR code of calculated text from configured pattern or function with applied attributes or timeseries values.", - "descriptor": { - "type": "latest", - "sizeX": 4, - "sizeY": 3.5, - "resources": [], - "templateHtml": "\n", - "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.qrCodeWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-qrcode-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7036904308224163,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"qrCodeTextPattern\":\"${entityName}\",\"useQrCodeTextFunction\":false,\"qrCodeTextFunction\":\"return data[0] ? data[0]['entityName'] : '';\"},\"title\":\"QR Code\"}" - } - }, - { - "alias": "markdown_card", - "name": "Markdown/HTML Card", - "image": "data:image/png;base64,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", - "description": "Renders markdown/HTML using configurable pattern or function with applied attributes or timeseries values.", - "descriptor": { - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, - "resources": [], - "templateHtml": "\n", - "templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-markdown-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Random}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"useMarkdownTextFunction\":false},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" - } - }, - { - "alias": "dashboard_state_widget", - "name": "Dashboard state widget", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAYAAABJ/yOpAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAIABJREFUeJztnXd8VFX6/993+iSTMmmkACEghN6UKij8RBF0XSxgb1hAFiyg7iIqu9YvKoLoIjbsywo2RIrSRKQlFBECIQkhQAppk0mZPnPv748xIwnJpE1IcO/79eJFZuaU59xzP/eec+5znitYLBbJ5XIhCAIybYMkSajVaoKCggCwWq3IfdK2VPeJyul0Eh4e3tb2/M9jNpt9ApH7pH1gNptRtLURMjLtGVkgMjJ+kAUiI+MHWSAyMn6QBSIj4wdZIDIyfpAFIiPjB1kgMoiieM532dnZlJaW1vu7P4qLi8nJyWlUPe2dBgVitVqpqqrC7XY3qwJJkrBarc3KK1OTf//736SkpPg+l5WV8eijj7aozIyMDD744INzvt+0aRMZGRnk5uayZMkSADIzM8nLy2uwzLS0NLZt23bO96+++iolJSX15vvpp58ab/h5QtVQgrlz5xIUFIRCoUCtVvPggw8SHx/f6ApOnjzJ1q1buffee1tkqAx4PJ5zrsLVF67y8nL0ej0ajQabzYbH40Gv11NRUYFKpcJkMtGxY0dEUSQ3N5fY2Fj0ej1dunShQ4cOvvLOnDmDQvHHdbNDhw7cfffdVFVVsWXLFqKjo7nqqquw2+1ERUX56tZqteh0uhq2lZaWYrPZ6NixIwAPPPAAYWFhgPfCm5+fT3x8PEFBQRQXF7N8+XJ69+5NTEwMAEVFRbhcLuLj4xEEwdcus9lMcHAwarUag8EAQGFhYY12BIoGBQIwe/ZsoqOjycrK4pVXXmHhwoUolUrsdjtHjhwhNjaW+Ph4ioqKMBgMBAUF4XQ6KS4uJjY2lokTJ/rKysnJoaysjF69evkOaGlpKSdPnqRbt26+A1hWVsbx48fp2LEjsbGxAW/4n40PPviAcePG0b9/f7Zv305JSQnjxo3jiSeeYNCgQbhcLkpLSzEajRgMBg4dOsTChQvJzMzkp59+4tFHH+XLL79k3759JCQk8Ouvv9KvXz9OnTrFihUruO6668jOzqawsJB+/fqxYMECFi1aREhICPPnz2fu3Lk1BJKSkkJ5eTlnzpxh0KBBTJ48mQULFjB79mwcDgevv/46gwYNYu/evcyZM4fU1FScTiebNm3itttuY/ny5Zw5cwaDwUBFRQXz5s1jx44drFq1ik6dOjFmzBi2bNnCs88+S35+PosWLeLVV18N+HFtlECqueiiizAajeTk5BAZGcnzzz/PiBEj+Pbbb5k4cSImk4mqqiqmTJnCnj17yMrKYujQoaxbt44nnniClStXcuLECZKSkvjss8944YUXyMrK4rPPPmP48OF8/vnnPPnkk7jdbhYsWMAVV1zBihUruP322xk8eHDAG38h8tlnn7F69WqARg17jUajbxh2zz338I9//IPw8HAWLlzI8ePHfencbjfr169n2bJlqNVq3nrrrRrl9O/fn0OHDtGhQwe6d+/OiBEj2LNnD/3790en051z9e7Tpw8zZswgPz+fN998k8mTJ/t+S09Pp0uXLtx2222MGjUKgMmTJ7Nu3Tpuu+028vLyOHbsGAsWLABgyZIlvqHl4MGDmTZtGpIk8emnn1JZWcmePXsYPXp0Uw9lo2iSQAAiIiIoKSkhNTWVkSNHMn78ePr27ct//vMfnnzySf75z38yZcoUdu3axU033YTNZgO8w4Mff/yRZcuWoVKpiImJobS0lJUrV3L33XfTuXNnJEli27ZtJCUlYTQaufzyyxk+fLivDBm44447GD58OOC9yz7zzDN+02s0Gt/fWq3W5xCp0WhwuVwolUrA65gXERGBWq0+J19djBkzhs8//xybzVbnyRkcHFyjnrMZO3YslZWVzJs3j44dO54z/C4oKCAxMdH3OTExkYKCAgwGg69cQRC49NJL2bNnDykpKTz++ON+7W0uTRZISUkJUVFRpKamkpeXR3Z2NuBthMFgwGg0kp2dTXFxMV27diUtLQ2AiooKQkNDUam8VY4ZMwbwrnisXr3a9/2AAQO45JJLsNvtLFmyBLfbzfTp0wPR1j81Go0Gp9MJgMPhaHL+8PBwTCYTLpcLtVrdYBndunXDZDKxbds25s2b16S60tLSGD58ODfccAMrVqxg7dq13HLLLUiSBEBcXBwnT570pT958iTDhw+noqKiRjljxoxhyZIlBAUFERkZ2SQbGkuTBJKenk55eTldunShU6dOREZGcuuttyJJEiaTyWf0O++8w7Bhw2rkDQ8Px2KxYLFYCA4OZtOmTfTu3ZuOHTtyzTXX0L9/f99qV2pq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", - "description": "Displays specified dashboard state inside widget. Advanced widget settings allows you to configure target dashboard state to be displayed.", - "descriptor": { - "type": "static", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "\n\n
\n
Dashboard state widget
\n
(Specify dashboard state id in the advanced widget settings)
\n
\n", - "templateCss": ".dashboard-state-widget-prompt {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n font-size: 32px;\n font-weight: 500;\n color: #999;\n}\n\n.dashboard-state-widget-prompt .title {\n font-size: 32px;\n font-weight: 500;\n color: #999;\n}\n\n.dashboard-state-widget-prompt .subtitle {\n font-size: 24px;\n font-weight: normal;\n color: #999;\n text-align: center;\n}", - "controllerScript": "self.onInit = function() {\n var $injector = self.ctx.$scope.$injector;\n self.ctx.$scope.stateId = self.ctx.settings.stateId || \"\";\n self.ctx.$scope.defaultAutofillLayout = self.ctx.settings.defaultAutofillLayout;\n self.ctx.$scope.defaultMargin = self.ctx.settings.defaultMargin;\n self.ctx.$scope.defaultBackgroundColor = self.ctx.settings.defaultBackgroundColor;\n self.ctx.$scope.syncParentStateParams = self.ctx.settings.syncParentStateParams !== false;\n}\n\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-dashboard-state-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"syncParentStateParams\":true,\"defaultAutofillLayout\":true,\"defaultMargin\":0,\"defaultBackgroundColor\":\"#fff\"},\"title\":\"Dashboard state widget\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"showLegend\":false}" - } - } + "widgetTypeFqns": [ + "cards.value_card", + "cards.horizontal_value_card", + "cards.aggregated_value_card", + "cards.label_widget", + "cards.dashboard_state_widget", + "cards.qr_code", + "cards.attributes_card", + "cards.html_card", + "cards.html_value_card", + "cards.markdown_card", + "cards.simple_card" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json index 8dac2b5f5f..694eaef0e0 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -2,216 +2,21 @@ "widgetsBundle": { "alias": "charts", "title": "Charts", - "image": "data:image/png;base64,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", + "image": "data:image/png;base64,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", "description": "Display timeseries data using customizable line and bar charts. Use various pie charts to display latest values.", "externalId": null, "name": "Charts" }, - "widgetTypes": [ - { - "alias": "bars", - "name": "Bars", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAA8FBMVEUhlvNMr1Bqamp5eXl7e3t8fHx9fX1+fn5/f3+AgICCgoKDg4OEhISGhoaHh4eKioqMjIyNjY2Ojo6QkJCRkZGSkpKWlpaXl5ebm5udnZ2enp6goKChoaGkpKSnp6epqamsrKyurq6xsbGzs7O1tbW2tra3t7e4uLi7u7u9vb3BwcHCwsLDw8PGxsbKysrNzc3Ozs7R0dHS0tLT09PZ2dna2trc3Nzd3d3e3t7g4ODh4eHj4+Pk5OTm5ubn5+fo6Ojp6enu7u7w8PDz8/P0Qzb09PT29vb39/f5+fn6+vr7+/v8/Pz9/f3+/v7/wQf///+dc+aLAAAAAWJLR0RPbmZBSQAAAcFJREFUeNrt3ds2AgEYhuHsaSOZbAvZi0r2YYjCJOW7/7txZhkcDNbM6h/vdwfPmlX/ybtmEorJErGCeJLadz3rkKPpZamaLTp925DHdFvSpKelU9uQ/cLKhtcdk7YqtiHruevtojch7ZZtQ0o1dcdfRqXNqm1IbVU3OaVamm/YhvQW5zIXOknnC5JUt7qEpE5fUv/5HVePy2UHAgQIECBAgAABAgQIECBxgrwGHBAgQIAAAQIECBAgQIAAAQIECJC/QRIBN0iQ+66voDMLuRp2fQWdVUhvNun6CjqrkJ0Dx/UVdEYhzXzfcX0FnVFIrlSZ2mx/LOiMQuqHh6k972NBZ/fv13G/KeiCQkIu4358EL8UdBafSGwuOxAgQIAAAQIECJDYQB4CDggQIECAAAECBAgQIECAAAECBAgQIECA/BrSufn0DjqjkEZmLXkWaUEXEmThXMeFSAu68H4j5b1IC7rQILfZTqQFXViQ1nRTL1EWdCFBnmYuJUVZ0IUEWR1xHKcXZUEXFDLwBR2XHQgQIECAAAEC5H9ChgIOCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgxiBmv+L6Bl9pkxYph15gAAAAAElFTkSuQmCC", - "description": "Displays latest values of the attributes or timeseries data for multiple entities as separate bars.", - "descriptor": { - "type": "latest", - "sizeX": 7, - "sizeY": 5, - "resources": [ - { - "url": "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js" - } - ], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \n var barData = {\n labels: [],\n datasets: []\n };\n \n for (var i = 0; i < self.ctx.datasources.length; i++) {\n var datasource = self.ctx.datasources[i];\n for (var d = 0; d < datasource.dataKeys.length; d++) {\n var dataKey = datasource.dataKeys[d];\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n var dataset = {\n label: dataKey.label + units,\n data: [0],\n backgroundColor: [dataKey.color],\n borderColor: [dataKey.color],\n borderWidth: 1\n }\n barData.datasets.push(dataset);\n }\n }\n\n var ctx = $('#barChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'bar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false,\n scales: {\n yAxes: [{\n ticks: {\n beginAtZero:true\n }\n }]\n }\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var c = 0;\n for (var i = 0; i < self.ctx.chart.data.datasets.length; i++) {\n var dataset = self.ctx.chart.data.datasets[i];\n var cellData = self.ctx.data[i]; \n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n dataset.data[0] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-chart-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Bars\"}" - } - }, - { - "alias": "doughnut_chart_js", - "name": "Doughnut", - "image": "data:image/png;base64,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", - "description": "Displays latest values of the attributes or timeseries data for multiple entities in a doughnut chart. Supports numeric values only.", - "descriptor": { - "type": "latest", - "sizeX": 7, - "sizeY": 5, - "resources": [ - { - "url": "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js" - } - ], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n var borderColor = self.ctx.settings.borderColor || '#fff';\n var borderWidth = typeof self.ctx.settings.borderWidth !== 'undefined' ? self.ctx.settings.borderWidth : 5;\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(borderColor);\n dataset.borderWidth.push(borderWidth);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var options = {\n responsive: false,\n maintainAspectRatio: false,\n legend: {\n display: true,\n labels: {\n fontColor: '#666'\n }\n },\n tooltips: {\n callbacks: {\n label: function(tooltipItem, data) {\n var label = data.labels[tooltipItem.index];\n var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];\n var content = label + ': ' + value;\n var units = self.ctx.settings.units ? self.ctx.settings.units : self.ctx.units;\n if (units) {\n content += ' ' + units;\n } \n return content;\n }\n }\n }\n };\n\n if (self.ctx.settings.legend) {\n options.legend.display = self.ctx.settings.legend.display !== false;\n options.legend.labels.fontColor = self.ctx.settings.legend.labelsFontColor || '#666';\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'doughnut',\n data: pieData,\n options: options\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-doughnut-chart-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#26a69a\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#afb42b\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#673ab7\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"borderWidth\":5,\"borderColor\":\"#fff\",\"legend\":{\"display\":true,\"labelsFontColor\":\"#666666\"}},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "pie", - "name": "Pie - Flot", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAZTElEQVR42u2deXwUVbbH/e+tM/Pe5/Oe82beNjo6znvjvFFnRkFxF1f2RREQWQQEEZFNFDUCOjAoCKjsq+xG1hBCWAJkJSEJCQGyJyQkZN+7urq7lvt+1RU61dWd0Emqqm911/2cPzq9pdP1zT3nnnvO795BrGENHcYd1ldgDQss3YYoiDcr+NQkLvqQc/sGx5eL2Q9n2aePt49/lRk1gBn0jNeTk+4kKb8iab8jGX1JzkCS9xYp/YxUriMNsYQtJUSwvs4wBovjhGtXXAf2OZZF2KeNs738hO3Zh7swr9ee/5uuLOFnJP1PJG8iqVxLWtOIwFpghfrgef7yJefWdex7k20v9euapJ6DpbL4f5AmtpKFpPFMWEEW+mCJba3ciShHxAfwaN2CSRuwvCazfyJXhpHq3YRrssAyLU92O3fmBPvJHNuL/XrMk8ZgdUxj/0hyBpPaH0J4DgtBsISyUuemb5khz/aeJ73A8ljSz0nBdNKWaYFFdQjFnY6xz5igIU+6g+WxrKdJbSQRXRZYNA2Xkzt53P7mCD2QMggs2S7cI6UtQsI/mhwsp9MVuZsZ/oJ+SBkKlmwp/00qVhPBboEVlEhKwFqPeX2Q3kgFAaz22esucnMzEXkLLAOjqUvp9kmvG4NU0MCSLeMR0nTeAkv/JEJDvWPZZ7bnHjGSqmCCJdnfkrwJxFVrgaUTUyJ39AAz+FmDkaIALDkx8QtStRXfggWWxhMVu3B2UJCiBSzZsOftvGmBpc1AdkrbbKeJwZKnLqTsLbB6NRwOVLAEFynqwJIN1RMCY4HVI/dXU4WKKBqoohEsacHYh7AlFljdTChcSApWnG4asOStxsaTFlgB79Ac+sHWvw89VNELllQo8fek4jsLrNvnFFw7NlGFFO1gyVY0l7YcPU1guZxsxPxuXW/7lDH2ccP9PsQMec6x8i+OpREd9wx73rlulVRRM3pQqIEFuzaWquIIasByONC/EOBlZka+BESE4kLpdcsi/D6Hi4mSZsD6uvaXDHxKrLop5Ofy2RmoKWVee0W6//m+zIgXQwQsKcs1mJ6tazrAcrDsvBkBUoVnYgdanuE6A4ud+w68qhIs9qNZuEeqTu7fR2xscKxahjudG9YIuVdCByxYdn/C2yywblE1a0rgXol9f6pr/0601rj2bPcP1suPCxU3hMI8L7A+nYceL0xRuC1WVznXfs2MGUxYO/v+2yEFlsTWCzRUdAUbLI7DXNKzgLozsFz7vseUhhyYlysc+bLIMPCPgFJ6dMoYZDTwY+jEWEq7/AoRHGEMlig4Pl/Y45WaX7DsU8aiRhkNgzINHrCkh2ZM4M7EcvFxQNmx+EOxuZEZ2j80wZJi+dHBbZ0NJljONct7kwLwA1b/PgjPxdpqhOq+YHXE/oOeERvq5PIbrAMCyZmZDyxY8QfhCJaUBe1dbskXLOf61e57PsOmNUwui5A2sF941OuFRyL5zIv2qW9g16i9dGLm5BAEC4Zu7LACi8/OtL3wmOZgiS3Nfn8dInelQ8RyAZ0XfGYanxzPDHyaO3Uc81xogoW8fJD2fIIAltjUiOxl77Phfmasb1cg/+kx6XcxNtzoSKI+31coKoDsh0RhVaVzw2rccHy5RLS1hSZYcpmNJFUSHjOWkH8NSQHNwfKlQRVjIQcmlBbLvdHwhnziOUycXPRhoSg/ZMGCpf/Z+Bqb4AXvrN0+a6rBYEl2q+Me+Qg8Kj2npZmdMy2UwYLlTwmrdIPo2rsjmLvL/ftA/koV2ocmWLC6A6EJVp29flHKkjp7ndotFubZXnmKwoqGUAMLxVuOG6EGlkjEiOTFAw4Nee3Y6MTKJB+3yLJzpltgGZGRN6rPxyCwTpSeBFWyDTw0dEvONk7g1W4xco8Flu5WtSN0wGpgG1+NGu0BS7Y55+bXMDVqt1h+3Tb4GQssHS35F8b0vhoB1vKLK1RUyQbaEioS/RRmBVxCY4HVE8sdHwpgZdde9kuVxy1uyN7kElxqt9jrDR8LrK6sOd7cYAmiMP30zC7Akm3W2Tk3bVXq194otw3pb4GlT+vYw3rXPugLVuz1k7elSraRUa+fv+Hzb+RyOT6db4Gli1XvMitYTt45/sRbAYIl28r01Q5eXaEGtb6QAiv+J+TKSHJ9CSmaRzL7eVVQQY/U16CEKwXdvyTF8yVZ79R7NdLfulvXQlMdwYosONAtqmR7N25WRVuF2i1WVtj0l+0zAqzkfydtWV7vVrGm/SHmmr8EIC+JeCf+C2Gvk7YMKTaClDc02WQF+ZT/7NWH0bMhUS+wMPGMOf5mD8CCjYgaFVd+zo9bXLTA9GBV73TDtJok/iu5+EB7KhwnDMhTSOpvOyz7RSmZWX+kvf0Gt3HUCspgnDWkYIa7ju9D6eSLXmpS6tbVoxdYh4uO9owqpVtkefVczSecNTdYrgapGl32brDy5dIb5k/utEYv61npNlwnYm1MUbjNlkntqam/kbpxsp6jthJQF7A4gXszZlIvwYJhRVneWq52DjXV9tcGmBWspH+TJh5VHhyTk9pj/lIqdIH+u2dq4VulJ99YITlHlME0xEg/9j7SQsQmcqYBC46s91TJNuzoyKNFx3zI5RxLFpp+VZj5qDR7MXmSg1M9hNAeAwqRHU/uR2r2kbqDkltEmO+qk3LoFFc96ALW7HPztAJLthXpX7NcMN2i9mAhikc8LjrJpSd8jtz5Z4kbKPdh/ehnzruTOKskfSxok6b8lx8oe3B2gSnAym3I05Yq2aaeeqe0RV1iK9bV2kcPMh9YWOW1pkvxuMSHz6OF70m/pTSik6hoPWk6K6kpO9xBgrOaXHqqt59HhzNXtAdrVcY3eoAlu8WY0hPq38fzjuWLzQRWwk9JU5z0Pshj+W1/gJwaFmsIs/x4z35S7JV2v/QO9cekpWXNHikN0cuPVDiTdrCwjkMOXSewZPviwjKbSy1PwKcl6yqppRlY4EaOacq+6FQ0BgNHB/g9+tCWTa4vdq8NS0nxgvaaY6651zWAd2qed9AYrNNlZ3SlSraJsZPzGvLVbrGh3j52KO1gVax25+TqSPmXHVbykcIrZUguEikuP2V6LxHmqjTh4Ta8IVJcCMKg0Y2Ma++9s9aCuRqD9XFShAFgwYYeGXm0+JivW3SuXEo1WC0X/M3zpbf0PF6UfkQnYBdutN0nPtauy43EWPbzWkggDaEXrFZn6+DDw40BS7YlF/7S6mxT7/9kZdj6Pxr6m9DwqkjWe3KtvY/8+BZKwQq8lkFDm3Bi8rWGXLVbbG3R9rCdEKxu8DXkyegEC004xoMFG3Jk+KHCI6KqTUAQeik6EnZgSQI19IGFKtARR0cFBSzZFqd8AV+sDroyL2riFsMCLGTXtFMx1QysrNrsIFIl2/gTk67WX/Nxi60QzbLACmxtkUIdWNuu7Ag6WDCsHiLzD/hxixvXWGDd3q5/Th1YaOeiASzZFiZ+2sg2qVeL13IC6aYPa7CynqELLFQhDzkygh6wYCgzvFSTpXaLba32qWMtsDpPOvxM2henB6ycuitUUSXboMPD9uTuQ6eQN1yic9sGC6xOTdodpwasAwWHKASrK7eYd7VbAl1hBFblOorA+ip9JbVgwcYeH3+pVu0WCcPY351ogeVzEuIkisAKpCs1uIaW642XN/sqkTgDOxMqjMBCZwclYKHCHblvysGSbUH8wnq2Xu0WC3JtrzxpgdVRv6pFk7QGYKEN0BRUyTY6etzFap/4FLqVs9+2wGo3NALRABauk4nAkt0i0rm86CPQtX+nBZZkDbFUgIW6KHOBJdu88wtq7WqlKKGkyDbg6XAH6+YWKsDakrPdjGDJupUJFT66ldIZd9PDGiy5+SzoYKE3y6RgKVaLnNotHtwfvmDlT6UCLMPKkfWz98/NrWaq1W7xRpnt1gka4QVWziAqwII+jNnBcrvFMUmVyT6boE72o/fDDiz00NIA1qTYqd2v+Rzx+YWlu3P3brq81bdtesaZ9+Cevr+6a1HK5wMPD1VmCjbnbN1+5Xt06eiEF5RIsKGudotHIsMLrIt/oAIsXO9u1h2MK24uUb6DUpoGSt2iKCq3t+W6CbQrQmW5sKkI96CvUBYdQfUVtmu0ZWtm3OzKtptd/cEhD1bKr6gACw3K3bpyp8vi3DAdgWrytNPvymdVQIYUD70RMx7ppRZnyztnZo46Nja9Gk12ZG3Wejz0WfISlO9hHYeahSZH03dZ63Dn1ivb8xsLNJ+33LqVCeELFmqUaQBL6a0CMVSmo0DeswuEgk9yqzH/o8RPiCRWc1Z+CK4QP6KtHrfhOjGTye1lNUwtfOjE2ClovF6QsFA/t+irWykN1O+iWRRVAGhKhpTjleHk0uPkwq87mv5CYVfHhGBhKsLE4/nxVNlpvAmKW3B78slpmJYKGguRBZAnJE/R87iYCXbOjiejRAclVlgxIOOPH3WN6BHt3Wit6MZ3AVVPKC+0JJOGaKlNHgkhibxhkqILRPpk5TRTlPtRAdahoT2+crPOzsXshYsHByffsz9fipTh4HDkDtwibnjEIFD9DGVlrN3gFpelLW92tLwe/Ybeq8W/pn2lYVMU4RolrVGQV/sjqfhWEquF4AzIQ00BIpvzf0dHK+xPTDljKaN4xONITs49/4HnThADKSx4PflUgeiS477d1ZjwcIwKXBWYRljmgVJzmxn3vnpLUdcBHTaokrakkvoo94T3uSRpBJ3IS09KEx48lEEz1k+pAKtn7YSYhwqaCuH4wIdyRQY3h/vBHHhCGyreH1kJ1WtBGw68wJPlzT4UiM6LX6A5VXC+qtTDyjTnq4fsC8461mc6owq5tJt8YaNQw4iMi/DGHKoFVwvVNQiHeFxt0Ryp0bTd1WrUbg89SxrAAgQ9kPRAHyJeixhcef/e3P24c3Xmt55oDD+WtZapTndCTD311HS8Q2pVGpaWUKZEGkJbqoZHvYrFqfLP3HPVddfati7s7rVt/Xbaxh9jP09y7MhxnSrlMqv50mahkRUdHA7pMIQ8HMBku0Iaz0i6WTdWSdLwODkHMjXpD5Hk/whYAv4uKsDC6qy7PQ7ykYV78/arHkISC/fvurZH/nHa6RnEWyIQ01hJc+muXOkJ2ISR979XZ35jczFadmEcGlZp80plxV3n7lnf1jVYgdgTu5jJMezSFMfeq64TJRJ5ZS1ik0N08kaRJwd5IK96d0eQB/IuPugO8txgQdiNBrDkFFTghgwWXoXQGycMeExe+k05NQ0ZBBiYW5+9EXMVnolTnDyv/TgxAndiwsNteMOUmxeQPoUYCTKuGoKFHKzyD7xUzf/vRg2ouq09tLXt9cP2j887NmS6oou41Eq+qFGok1ytaJSrZaRVrS2bCrAikhd167JhBvJ9E0w/8qNIZV1vaa9gRIYdGzuqVadMlbxz3MA2ELd80kcJn2hF1YnrXmVuZS3Cn7fZDKAqEHvQTd4n8Y5v0p2H8rkLlVKQV2cX25wiJxCqhgZgKaNvDSulxsVMDMSrQq9Bw4p7TJzKPw3h0dO7GUqouq3dt77tlf3MO7Hs12nOfddc58v4q3VCRavY6pRcrWg6sLArHALVDVKmI3W5UvSB5cjwg3azUBWI/X5z24DIjiDvXBl/rV7wBHkibWBFl8SEAFWzz81X9kwjlH47hg0lqgKxR7YzWNVevMlTARa2is1O1fiYt1Qpq0UJjnCjymMplXSAZa72L7+p2jaXl5DpxkuusKUKVsuIVICFTQ/PSs10hpRVNVPj5dmLuF+vC1+q/m+zjZYYy9TVydjkVv4h2KX57Ya2cJ6uhh6wUwQW5aIgnVl8RaLyr0BO6MEttnCmCjYvzkERWEeKokxH1Z7cvco/AXvJj+9kwpwq2O6rLorAKm4uNhdVqzLWKD+/nSNwARZVsNx6gSKwkAFC+5RZqPowYaGyXwPbcJOPsxZSsPs32bTalNRM3BZVnaagasqp6aq+Z9RXWUjJNuaoZmeAaQYWKhTop2pU9FgUzis/9pp0p8WTx1amOqkDCwfa0J6yOjKs3l0N0bHmKODutnhSWEY1Tx1Y8C/BPfLktoYKQeUHTq7gf7PegqnD/rjNpmHVl5aHNOHsU2qpSqvyUvHLbxD+EPYpK5XNPcPSVd3gGWfK4+ikCi38ys9ZZRMf/d6iSm3HizhKwTL+IMxADCXO3h9SfGGflQhVG6ICfDOUgtWDMmW9DU36yto91O++cdRKhPqxt0+w2pKgMVhny8/TQ9XMuFnKdlPwNeeMlQj1b2hWoxosNNiMiKJibYhDmvBhlJ/trylWyqrT9aDmvRgag0WkdtNvgk4V6sMgdaT8VHuvuiyAOrPFiQ7NMdAeLBo2pMtby72Wq2g3XWcB1KmhmccEYGGg4y+IVKGRVflhcmqF322ykgudGhoVKW3/8h3Q7AsWVcdLTig/STlN7aZ0GqZz04CFQ7b005/twjZd3qJqN312j5Wy6srw/eikGaELWMRbr9YY+wJakoqUlYMnIw5aKavb2F6N6kWNAwviad1VU+5du+k8Vbvp9FgrZXUb67PDxnLEZGBhHCw06DxfSHOr2k2XJBpRu4d+npkn2VUXnRHxjs62iXDxVH36D221QUALSTWIaYVGebvRYDl5FxQ79KYKCmnYo1T+3i1ZRqSssI19vdlrjlQmYCF7NOsUe76cRyHKD7kuZe3vjVYBC1UIxbQ4xL47JLagvGX8CgNiJ7oK1OgIFrmlpK1ru2mVzesMHGxNGJOyinbXAnyb7vyfjW399zKVbZKE1ZO7GJkem0t0/2tJH0kJ1oRjdjyAplB8SMgPQQoLdy5NdmbV8AaDBalLXS+9vmAh7plxRsdeVpXUVnqVce2mQArikZ5ft/2y5FZkHRGA9WMeB4YmRrMqsKbGsJjbZHFACAzBZaPnDLpqow4bus4YGMnorWp0h87vT7LrLutE1dkb57wy/k0CwpdgxSuoR8VneHm/V6Q1LsquAuuR7dJkFpnr2nDJiRkOzz9bxkfmGrrdhGpsTfRkggwWxtLU5Xq3mzbYxaeCoZA2JYZFSAcpUdHtFlWP+oIl97AfLeBiiqUpDSJp+OQG/z98eM5hwEU3AixoskP/WEOqVqSvUr4/2k2HBandFLo0jDucQjLWN8HhFyyl/Abar1HJgynk4e02Y0JDQIyPGiJguaP4WK2o+iBe3W46JdgKaYjZ8+oFBE+DIpnAwdqZ44IDHfCDFPjjaRAPMiCjC+VSY664QWAhJ66J/uzkU9NU7aYLzwWn3fSBLTZMOZ4fITiLD7Mi1RkgWHCImGif2cMk3eBPl3K/32zDJUcaQtfPPP6YnRg17jDsN+EUiV624Y+KHqNqN/0uI2i1e/Bi2DW6/1bdBBpffSnvDCysCiH+CQla3MY2+V+SpVfNj3Og6ly/D/ynbbZaRgxBsDBwvlLv2k3rle92tDCY7aabsqT8QkYVj1wUwnZMPwi2VM0/nYE19qi9oEG4b0P7cjK2hEMvA3SOURel30rwdCln5LU2FCyMrzN6qN1d1FysfB/oZN4XVIW0e9e34UQdOXKXCKvmh/xoDzx493z4wT8y1bb28H/0Eb1irIh4h8EX2miwcAxOD+T/cAKF8k0QKSvjmyAaRCX7fm/rZSHhPe43uVe3tuyX9jMsR0IcLCJJnNV065zBHwsOKl+O/+/HrHbT7uQXcLiG8Vc5CGARt4J3gIcM4uxn5QtxtsdL+63avW60oaZW8kG5xMEBCyOqOPq2VH2c9Jmq3RRRi4ULJYUxlIKFseHypi6owgH3vGC1m/bcliQ5gnhxgwkWah+WpX3pv900+k3UoCqf/FWq1W7aDZt9mhVEEqZgub0bF5G82F+7abPyacjxWKx0S4iBD/Ypc0EGS05ALEhYqATLc16hPFBYco+lkNYdHVEHH/SrSgFYMlsfJ0XIVGXWZCkfstpNu0uVJ2drgSUNdENAAulYSbTyTpSHo57EwiVAe+s4S8NcRRdYUrwleqWHcTojasktXAI/qoQX6bmYNIHl7RzJq4eslFWghn4ykbIrSClYM09ZKatA98KDmAU1H1iJN/g/WmIeAZRYBWvHxqxgYdxsE30LUSzzmKem2QKrB0tFIrd0Wqaq2sPXYudovnR0gyWPY0VcEBsGKXR/BteChixYGPV2EdsUFlUo7pDLTS2wtBzoY3kgXM8pwVIGDdOieS6WmcAi7gN257o7PMMHKVQ/o3e5ySGa60qZDCx5QJslTE7axdIvs5o34zUyJVjEXfd3II/rG7rF75CiOFzACaJJr49pwZKHSyAH8zlZlSpkDP8t2y67nLypr4zJwfLgBakqCE2FwCy1I8fl4EPgmoQEWPJAqwVU9tDzacbQHh8bWoTmdXyhDJZnFDUJixIcpjhA9cEtNuTQc+uF0LsKIQiWPND7C3GzGbEsdGZp4wmypZBcO1nCuYRQ/fpDFyzPQKkuNoWgihb05CqSnJBSxlpP27NMLbCCPFBgmV0jrM1wojDcMA1cFOyjYhEiR/jVghg+X3Y4gaUcWMxfrhWwloSiFYpzNOQMJEGTGLlytKwheOKF8PyCwxUs38kM4tgomkPSFSdNoH58UjSLmQY6ETg/AusACKzBk3oM4sfP72NeO2yHvPYHZx1opoVWEV5ew4jWl2mBZQ0LLGuYbfw/UfikHjIsMFkAAAAASUVORK5CYII=", - "description": "Displays latest values of the attributes or timeseries data for multiple entities in a pie chart. Supports numeric values only.", - "descriptor": { - "type": "latest", - "sizeX": 8, - "sizeY": 5, - "resources": [], - "templateHtml": "", - "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: 'Roboto';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n", - "settingsSchema": "{}\n", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-flot-pie-widget-settings", - "dataKeySettingsDirective": "tb-flot-pie-key-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "pie_chart_js", - "name": "Pie - Chart.js", - "image": "data:image/png;base64,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", - "description": "Displays latest values of the attributes or timeseries data for multiple entities in a pie chart. Supports numeric values only.", - "descriptor": { - "type": "latest", - "sizeX": 8, - "sizeY": 5, - "resources": [ - { - "url": "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js" - } - ], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'pie',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n }); \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-chart-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Pie - Chart.js\"}" - } - }, - { - "alias": "polar_area_chart_js", - "name": "Polar Area", - "image": "data:image/png;base64,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", - "description": "Displays latest values of the attributes or timeseries data for multiple entities in a polar area chart. Supports numeric values only.", - "descriptor": { - "type": "latest", - "sizeX": 7, - "sizeY": 5, - "resources": [ - { - "url": "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js" - } - ], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n\n pieData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n \n var floatingPoint;\n if (typeof self.ctx.decimals !== 'undefined' && self.ctx.decimals !== null) {\n floatingPoint = self.ctx.widget.config.decimals;\n } else {\n floatingPoint = 2;\n }\n\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'polarArea',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false,\n scale: {\n ticks: {\n callback: function(tick) {\n \treturn tick.toFixed(floatingPoint);\n }\n }\n }\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n try {\n self.ctx.chart.resize();\n } catch (e) {}\n }\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-chart-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fifth\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.2074391823443591,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Polar Area\"}" - } - }, - { - "alias": "radar_chart_js", - "name": "Radar", - "image": "data:image/png;base64,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", - "description": "Displays latest values of the attributes or timeseries data for multiple entities in a radar chart. Supports numeric values only.", - "descriptor": { - "type": "latest", - "sizeX": 7, - "sizeY": 5, - "resources": [ - { - "url": "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js" - } - ], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \n var barData = {\n labels: [],\n datasets: []\n };\n\n var backgroundColor = tinycolor(self.ctx.data[0].dataKey.color);\n backgroundColor.setAlpha(0.2);\n var borderColor = tinycolor(self.ctx.data[0].dataKey.color);\n borderColor.setAlpha(1);\n var dataset = {\n label: self.ctx.datasources[0].name,\n data: [],\n backgroundColor: backgroundColor.toRgbString(),\n borderColor: borderColor.toRgbString(),\n pointBackgroundColor: borderColor.toRgbString(),\n pointBorderColor: borderColor.darken().toRgbString(),\n borderWidth: 1\n }\n \n barData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n barData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n }\n \n var floatingPoint;\n if (typeof self.ctx.decimals !== 'undefined' && self.ctx.decimals !== null) {\n floatingPoint = self.ctx.widget.config.decimals;\n } else {\n floatingPoint = 2;\n }\n\n var ctx = $('#radarChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'radar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false,\n scale: {\n ticks: {\n callback: function(tick) {\n \treturn tick.toFixed(floatingPoint);\n }\n }\n }\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n } \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n self.ctx.chart.resize();\n }\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-chart-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Radar\"}" - } - }, - { - "alias": "state_chart", - "name": "State Chart", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAB9VBMVEUAAAAhlvMilvMymeE+nNVDoetInslNq/VZqOdpuPd3d3d5p5Z5suB6enp8fHyBgYGDg4ODqoqEhISIiIiKioqKuN2MjIyNjY2Ojo6QkJCRkZGSkpKUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKCgr3KhoaGioqKisXSjo6OjvsmkpKSlpaWlwdempqanp6eoqKiosGOo1vqpqampsGOqqqqqsWOrq6usrKysw9Wurq6wsLCysrK0tLS1tbW1wbK2tra3t7e4wau5ubm6urq7u7u8vLy9vb2+vr6/xbTAwMDAxbjCwsLC3ejDw8PExMTFxcXGxsbHx8fIv3jIyMjJycnKysrK5vzMzc7Nzc3Ozs7Pz8/QuDnRzcHS0tLT09PT39HU1NTU6/3VzLLV1dXV3sjW1tbX19fYuTHY2NjZ2dna0Ira2trb29vc3Nzex4De3t7f39/guyvhvCfhvCvh4eHh6Nbi4uLjvCTj4+PkyXbk5OTlvCTm5ubm693o6Ojpxl7q6urr6+vswTTs7Ozt7e3uvhju7u7vvhjv7+/wvxjw8PDx8fHyvxX0yTv09PT19fX29vb39/f4wyL4+Pj5+fn6+vr75J37+/v8whP8/Pz9/f39/v/+/v7/wQf/xRb/3HT/5JH/9tz/++/////APs7XAAAAAWJLR0Smt7AblQAAA1RJREFUeNrt3dlT01AUBvAEd8UNl2oLrdrFolZArUul1hWlVhQXFAUF1xaxIqi4FUQUrDsUClZi4nb+Th96S9NQkjDOOKZ+3wuZw8md++M25OXMlKOccJQnTUYocdRqdw8UAqTfOjG4lvr7uowOqbtAtG6o5OiGFoNDapuJHO8tFK4xOKS9msTVgoUiaQjPclnSl01snZ/y4ly2yBNZbRarbc+3yvdpt/hLawOPJp9ur7O0mRiEmzFkY1M6N/4I8qJpulzR2sBx1sgRjQuyA2pk+STdylw2VqR/FPFfWdcCvjhduiM7kVF2db1Nuspu7JGes+I76SRba7f0Wfnn/6F6It+mfo7m8TvYvh7KTiT3PQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggPwXkFa7e7AQIP3WiUFzdl7LuBDFvJZxIYp5LeNCFPNaBn7Yc+e1KlheSYcrFCniL7LZqPl8cbp0TjavNcqu9rZJB9gdPdIJVnwjbWG1ndI95UzWM9V5rS9Ti3P4VWy1+5jXAgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAMK+FeS3Ma2FeC/NamNfCCxEQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBA/hZkTKSkwSGn7VUJOmOLV241NiRSRZ2uYcvt+Io+Y0N83USm+MqGWEnU2JCqXqI1ZE+QhYiIy36tXR5INpOQbB5kftcmK85mtdey2iJekeWqX6/3kc+TLCQTrq6eEuZCgKTsbnNXBsIZOERJMedNQnry73XpW8UAKVhIyBPScVfQE9RqEaP7iMT6g+pdw7vKbxKl3IJqV8K7OUqXPHvG9UMGykk2lz1d4g5yvNXo6QqZiA41aHT5OwQTUWBJSrWrvlMwj1jFU2f1Q8K1dCysCUmVhdenNLvMRKZt3hGNriEnxfxlGqt9aPYRkb9bP6Q1SMFrmltMukKuMT2QpckWjc/WhP2lYB/TgjzdHyCKVM/gGXnsp+qY5hbba+hIVA/ETL0+9SepsoNiTs/igGpXZIhKqVv9QVJARIfXIWqfiM1nS+qBnHdae1V7Qss8ngEijRO5a6sMCAtdqv+HfgPwpNPbU6ipOwAAAABJRU5ErkJggg==", - "description": "Displays changes to the state of the entity over time. For example, online and offline.", - "descriptor": { - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "resources": [], - "templateHtml": "\n", - "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}", - "settingsDirective": "tb-flot-line-widget-settings", - "dataKeySettingsDirective": "tb-flot-line-key-settings", - "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", - "hasBasicMode": true, - "basicModeDirective": "tb-flot-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}" - } - }, - { - "alias": "basic_timeseries", - "name": "Timeseries Line Chart", - "image": "data:image/png;base64,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", - "description": "Displays changes to timeseries data over time. For example, temperature or humidity readings.", - "descriptor": { - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "resources": [], - "templateHtml": "\n", - "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}", - "latestDataKeySettingsSchema": "{}", - "settingsDirective": "tb-flot-line-widget-settings", - "dataKeySettingsDirective": "tb-flot-line-key-settings", - "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", - "hasBasicMode": true, - "basicModeDirective": "tb-flot-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\"}" - } - }, - { - "alias": "timeseries_bars_flot", - "name": "Timeseries Bar Chart", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABOFBMVEUAAAA3oPR3d3d6enp8fHyBgYGDg4OGhoaNjY2RkZGSkpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKChoaGioqKjo6OkpKSlpaWmpqanp6eoqKipqamqqqqrq6usrKytra2urq6wsLCxsbGysrK0tLS1tbW2tra3t7e4uLi5ubm6urq8vLy9vb2+vr6/v7/AwMDBwcHDw8PExMTHx8fIyMjJycnLy8vNzc3Ozs7Pz8/S0tLT09PU1NTV1dXW1tbX19fZ2dna2trb29vc3Nzd3d3e3t7f39/h4eHi4uLj4+Pk5OTl5eXm5ubn5+fo6Ojq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fH09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7/xx////8KXFhiAAAAAWJLR0RnW9PpswAAAvtJREFUeNrt3GtXElEUBuDpZlAqylVHCyMwyi4SpkaWAl4qk8wwUhJlmOn9//+gLzbKbc6AAm5991p82WvPOedZZw6zYM3aGupCQ4tYkZDSsKDrocNVvz8jHQKYY1ZiT/6OAJllRPSpsnzIqIG9SjYmHpKfAzI4CIuHjJeBtyHfZwCatiI1/m+BYV2Dw35NniOEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQUp/6Wx+DgbRbxOAhEb/fXJ98akiH1AIAHtSWlqRDSvdDGcOHnYR0SPV7zVsaQyHeO0jTJapEV5C9bUz9fIjsvHRIefhRxHqpjxzA3ftajXOudHGJOtHV+1rV04/wHenDc2QQkFbLJISQwUDaP90IIYQQQgghpAvIxRO9gFxG4lZ9XBFIx6sihJD+QVRjEEIIIYQQcoMgahkhhBDSe0iLX3ydQdSLIKR9ghCJkIYEIYQQIhPS8d+AhPQboppFw9HMxBrs/lqCIbObpgezP67YjnQ8iwagHDzrryUZUgsUz/prCYZY05vn+msJhiwM6XrR7q/V9OJU4xQuEpcxRsezNPbXEn3Yzz9Hulg3IYT0C3LxxI2CNCQIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQlpA7P5a0iF2fy3hkLP+WsIhVbu/lvRby+6vJR1i99eSG6ffu1XHxlRNcfWq3I0iIK4tJK2nXVyV0lOqEmvjGWAtvnCu+jMzkQWMiOlYdRSd3MAH/UnVPWR/ApFfSkcpiOBvRc3XtAeYW1ZUJbZMD5C8azhWLX4xvZVR692Se0huHm9ySogxkhs3lFVewPM4WlFUlUMoJEYUox1+jAFIbLuHrKaQWlMu8TicDp+4gdw7/qS4t2qBohk4UUF2nieBfLyDM/ItgXhBucT113i14QbixW7M+SRNb6EQ0u8kHavyZQxj2/kgNUCsYDRoqXfEF/Mdu4G8D43uOtakh3R9H1DsyKZvOmneDjt+D/0DTzolrPMHmggAAAAASUVORK5CYII=", - "description": "Displays changes to timeseries data over time. For example, daily water consumption for last month.", - "descriptor": { - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "resources": [], - "templateHtml": "\n", - "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}", - "settingsDirective": "tb-flot-bar-widget-settings", - "dataKeySettingsDirective": "tb-flot-bar-key-settings", - "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", - "hasBasicMode": true, - "basicModeDirective": "tb-flot-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":true,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"defaultBarWidth\":600,\"barAlignment\":\"left\",\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\"}" - } - } + "widgetTypeFqns": [ + "charts.basic_timeseries", + "charts.state_chart", + "charts.timeseries_bars_flot", + "cards.aggregated_value_card", + "charts.bars", + "charts.pie", + "charts.pie_chart_js", + "charts.doughnut_chart_js", + "charts.polar_area_chart_js", + "charts.radar_chart_js" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index 5ecee7ed9c..249c0dc439 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -2,199 +2,23 @@ "widgetsBundle": { "alias": "control_widgets", "title": "Control widgets", - "image": "data:image/png;base64,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", - "description": "Send commands to devices." + "image": "data:image/png;base64,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", + "description": "Send commands to devices.", + "externalId": null, + "name": "Control widgets" }, - "widgetTypes": [ - { - "alias": "rpc_debug_terminal", - "name": "RPC debug terminal", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAWcklEQVR42u2dB5QWRZeGi6QgklFUDJhFMCJGRFHMomMAMStiBvUXRTFgRjGLERMoiIBZ1GVXdNcF4yIe5agYdvVnfkAwIAoiIs4+/72na3u+NN8wAzsD73vmzOnur7rCrbfuvV3VfSuUlZXNnDnzmGOOadKkSRCEKgAKlZSUzJgxA1IFWNWyZUsJRagutGjRAlIFdJVkIVQvevbsGWQBhWpH06ZNJQRBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEITlRK9evc5KwAdAnTt3rlevXvz1oIMOir+efvrp3bp1a9WqVUYOderUOfDAAy+77LJLL7308MMPX2ONNYop99577+ULyaOPPrpwsvfff59kG264Ya0W8uDBg6+55pqq5PDRRx8hh3XXXbfWtPnzzz8vK4/PPvtsjz328F9ffvnljF9///33q6++Ot6+/vrrv/fee+kEX3zxxeabb15biHXqqafecsstK5S4a6655pIlS/7444+11lprtSPWEUcc0alTp/333//hhx/mdNasWa54nFj9+vXj19122+2MM8747rvvuHLiiSe6rnrrrbc4feyxxzp27Aifbr/9dk6nTp1aW4j10ksvkf8uu+yyQoVM/piC1VFjbbbZZtGu+RUXhBMLgxjTQymuvPrqqxzvs88+HH/44Ydps/jJJ59wccstt8wua+ONN7722mvvuOOOQw89NJtYlAIv7777buwp+aSJtd122w0YMIBbUDDRUjMYsL8bbbSRn+63336ctm/f3k/XXnvtiy66iFsw4htssAE/MWzSlWncuDEXUc/kT7L4cXn9+vVxD+65557bbrttr732iunJ7dxzz91mm22o4QEHHECIA27v0aMHgkKFDxs2jIsk23PPPWng0KFD995773jv3wx+3Ldv34svvrh58+a06L777jv77LMbNGiQtgBcf+CBB8hz6623XnWIBd59990CxOrSpQtX3n77bY6HDBnCMfJNZ4gfRl9uuummGQWhz3788UfS//nnn/z/4Ycf0sR6/PHHOcVkLF26lAMkmybW3LlzMSVuaseOHes/jRkzhlPI7aeuLI8//niOGzVq5PxetmzZX3/9VVpayvFdd92Vrs86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", - "description": "Allows to send any RPC command using its name and parameters to device. Useful for debug.", - "descriptor": { - "type": "rpc", - "sizeX": 9.5, - "sizeY": 5.5, - "resources": [], - "templateHtml": "
", - "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n", - "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-rpc-terminal-widget-settings", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"RPC debug terminal\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "rpc_remote_shell", - "name": "RPC remote shell", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAU6klEQVR42u2dCbSW0xrHtyZDOpVkyFCKIplCZpLMIVJIhsgYmSUakLniInOGyFRXaCn3ilws89Q1RrpRSq4k15Tx3N96Ht9e73m/4Xzn9EVH//8666z97u99997v3v/9PM8e3meH8vLyWbNmdevWrUGDBkEQFgNQqGvXrtOmTYNUAVatssoqqhShVGjcuDGkCsgq1YVQWnTv3j1IAwolR1lZmSpBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEAShmujRo8fxGfAB0NZbb127du3465577hl/7d2796677tqkSZNUCsstt9wee+zRv3//c845p0uXLvXq1ftLVtQ222zDJ50PP/xwlZ7abbfdeOruu+8mXKtWLcKzZ89eJoj1/vvvl1fEe++9t9122/mvEyZMSP26aNGiQYMGxcfXXHPNl156KXnDBx980KpVq6Xh1aD7FVdcsdVWW4lYfxqx9t9//y233JJauPXWW7mcM2eOCx4n1imnnMKv1Oyxxx47b948Yg4//HCXVc888wyXt99+e7t27eDT8OHDuXzttdeWhlcbOnQohTnmmGNErD+NWC1btox6zWPQiZFYKMR4P5QiZuLEiYR32WUXwq+//npSLb711ltEbrDBBqmMDjnkENTluuuue+mllx533HEe2axZs/POO++mm24666yzVl11VY9s27Ytd3bo0GHvvfe+/PLLhw0btummmxJ/0EEHXXfddRdddFGbNm2SKcPpCy+8kET69evXqFEjj6QzPPXUU5Rk3LhxJ554Yry5c+fOJHjttdfisIDS5qyTpk2bnn322eR1/vnnR+kbiUV2Q4YMufLKK7fffvvkU+uss86AAQMoxhlnnBEdIIhYLWPMiy++WIBYO+64IzHPP/884csuu4wwJEgmiB2GQbbeeuulMpo8eTI3z5w5k/8kS8xOO+20cOFCLr/77jv+z507t3nz5sT37NnTpSb/f/75Z/5///33Dz74IIGffvrJ74/pQ5pffvmFyB9++MGbbf311yceLxcew81Tp071m2l1T8TTueOOO7IrZOONN/7qq6/IlxTQ+9y57777RmKRPsl6qX799VdeNhLom2++ie+CH4S11lpLxPqdWMsvv/zRRx9NfX322WcrrLBCNrHQj/fccw8xI0eO5HL06NGE4UExGTmxkCKICiRQ3bp1P/nkExpp22235VeUbGwAJ9Y777yDDKhTp86YMWO4/PTTTykkbXPjjTdy6XYeMT/++CMU3HDDDRlzICr46emnn86pCg844AAuKcbKBm7jMiV1AGqd+H322Ydw+/bto0h2Yi1YsGCHHXZA1CHSuKQ2+Im6osa+/fZbDAYu+/bty0+33HKLiFUBn3/++c4775w03j/88EPMpjfffJOunOyOLkXQUMUTy5VarHFvGNehX3755UcffRSJhcLynw4++GAu4ZNf0uRcjho1ijDCkjAKKCaCmKFjuG+LFLHQiVwicaNq5hJ+pMp52223EX/NNdegtYO5YXH1mrKxUJFcPvvss4QZCxPmQf8JAkGyt99+W8Qqv//+++lh1AXhPn36xF+TxEJFPvDAA5hE0YBwSz95f6XEwn7yS9RleRbQO5AjRSwkXJSRPtbj8s4774yq7cADD4y50PDEQIJsYiF4snOMyUbQZ/zO3377jVc+7bTTEOTZxIJ2XL7wwguEse2yU0Yzili/q0LsKmoTM8irMqeNlYSrA0aCyUgMWziE6V2YWK77nnzyyeMrokrEuuSSS1K6mASJwU7KJtarr77K5eDBg5PZRdmcAkqNQcaMGTN4BMFcmFgMFAg//vjjyZS9v4lYvxvv48eP5xJjpRhi8RSGM6qTYZTHIMzmz59PJBqkMLG22GILN6QwtjyGsd5KK62UrQoLEMvVGaZezB0dhBnkcyUXX3wxv8YR6M0335x8NS9D9kuRGmaWh+lgDC9c9hQgFtN+borFueWNNtrIjVQRq2UcumOjfPHFFw0bNqyUWIDpAB/rMRWJ/Jg+fTqXDOYrVYUxcWbCTj75ZB7/+uuv77rrrnzEuuGGG7KJReNhVCFlsW8QG2+88QY/MUfgd5500klcvvLKK6TPZevWraEdxj72EzeTO2+abbyj7t30ZjB4wQUXkPiUKVMKEys5NCEvSk5G8FjEqjDd4KMwWFIMsdBcTEH5rCkgMHDgwOSiUAFiMTSDED4pQBtPmjTJ7eXiieUNTMo0P5FIF3KPs1NIzXfffZd4+onHIFqcfICxAhTMnsrC/xg8YKLBb0PBrb322pUSi6foFf4UAvvRRx9d1qcbSgKah6nO6rlFRd1A68V0JweHaOmoVSNoS+YsXMMm5z9z3pzEiiuuSKn4X6ViIEF5Sv4WBUEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQajRWW2211Q18tF5z34KP63N6C8dvIK4W8rkkXXqaoLBbgKUIeE9Meuno1q1b0jtIErgbxUvH2LFjs/0TLWng1qyqmeKM/oyKwB0X8TgdjS4Fk8BLL6+G+4nFL+2pp57qOeLapLROHPB+iHekmkEsamGvvfZKsqdA0WndP55YSJH77ruvqk2OF5DtDHgsgpcE8CZagFg4JsElbpESa7PNNsOvWj7hQfPvt99+eN7CS2q2H8plhVgcNoGUwnsdlML3EH6t8M0SzDUyrqHw8LnJJpsUIBY340kRx3kcaeEqkurGGRoes6GsswHXyMhFYnBie+655x555JH+LK70qHpikiIzBTo9xSBT/A0hA9xFMbrssMMO8yzWWGONwi/IbXhBipcQCxempEa+HH3gL+UC5vTTT09KbvLi1XD/584BAQ7GOXoDf4W48sKnd20DL457N9xn7r777s5Lmt8fwYFR9BOGCyQcpfIu0R0Xmpf03TO5CzYq00vCiQf0dn7F1039+vVxC00WMLUmEYt6OcKADzs8++CtioqGZ9dffz31SNfEDVWUFiliIRXwMAtpIB8V5O+MxqH68KqN1zJ8l+HWB7/wONDCTSiOyKhx3C4iHqhoKh3/sNQXWWC65SweUoTCkGnHjh3JxUtywgkn0JBkQdZ4vI1eLYskFo+TLH3GXeLyOCnjAAx/a/G2QQY8DNIN3PkxHOJZxB7ulpCC7uO0U6dOI0aMaNGiBXfSRd2FH81PfeKhHjbAv2De/ahecuH18dq6+eabB/NFCBepc7jrPgfhEPdcddVVdFRS4+3oQpQW13b4hEZZU8IaQyzenw5BJeKok6KjdDBjkQRQwVUJdhWCPSex6FjutC4JqAkdk2GIhQCgRiBfMEeMtI13Tc+CtqEY+UpIa6WsH4gYpQhhXBBWiViuCsk36dYWqqWIRbsGO9CF3Osb6BJ+jhC+3bzA+OWma9FnIFz0pAWxEDa8NcTy8ztgCb4Fo4qIYYQWTEKrIgVj1vAsCnVA96Ndap4qRE/BIbogbm3pN+54DoGP1Dkgg9iKKWJRrQi2bDsg3o9LY3p2ili4vYNYAwwxiwL1lSIW6ubee++NJ1PAXXetXlViUbDoIrAAsRiIkTueBwkjh2hmagahEq1y0qHeeDUY5n7nYg2gpr26YFL0g0oi7gSV/zAPT+NIX47VSBKL42fiJXVIPdc8YiGWIQf1iDIi4L7UqVP6EKILswlxFSsxRSwqjjbGjSKNjeLzM05ICvXq3RGXk5xikpNYxLhHSc8CeZCvhKiDlMSCK+5/G6OELAoPJkpFLN4RHqD1EFrRxkfe+IEovEgU1ZFY9FXqB1WLsQUXsT65jUrAyEt2D+qnALHgMdqQ+5koQWTWGGJRoVTcUUcdRdERV5wP42YHYgyzABJQxd7qOIIfmwEaM2pS7qG1kHluJ8Gnq6++Go7Sg6lQr7hsYpEFcpEs0IyY0oU9MtK5aXXyRY84n8iOg27IAturqsZ7iliu7CK8BnJKLJqfSkD5YidhOVBjDEeQKIgxWMXr+DgRYo02EOlaDD5hXXCJuca7+G28C6+POUW/IkDPhI7JkmCEBTumgF/JsVevXjVJYhUAOqiYaRhqDZmRGqjzYE7/tinAp2rP9MDOP3KqFkvO+xhg3ADDfFDJi/P6xbgqpS+lbuPd3d6vtIbdg7XwFwSuvJFVHDSEAMYqQrDVmElwYSkHYgMrCssJ9bSUL/4IgiAIQj7k292QAnMKvpUjGlIEPCb7rHUh97gsrv8z4etzQgzFPYblWxYl3J5gUMMKHUNu4n098Q9DNXY35EO+RegUzjzzTBYEmTeK65gc6EoMk/XMdIg2lYMRL1MmTEcxvcvymU81cbAlc1EsesAqJlF8MoZ5F2rWl2hYySm8QldCVG93w2ISy8HEXmqBnIUKEasKxPIpGV8hZtYEYsW5YOQTM7/MGFHLvnpKSyPS8g22kWo8wiIGM/I8SEsEm6NnhpA0SZkJ/WCH2zJryrQygywmCb39OMiUhTMmuCGxz0nm3N1ACXmW1ArvbqDM3MYEKWtHrDG73M3e3RBsAp1ORUbx8FURqzTEYnMfY2k44ZPRND+NzUwgzca0MusnvpgTW6IAGIozZ830Oue2kQ5kxaZhpYimZVKe9YpDDz002Pw7EpGFDqawmRyCZzQ8y/isAbDgDy9p5pBndwMiE2aQGhRBN+WjOIIWEqDCfOuBT05m725ArSOVEWMsSTGN7p1HxCoNsahQ1A0bGXy9D0L42gWrEOxSokkwWrkt3+aWFFCXpBZ1JYQgKd/IAG9QsiGzsIPxhBBCC7OChFnt+whCZiHFw9m7G2A/fcATZMXNy5wNxCqM8d1m8fSv7N0NLANzgJ6nBl8RmSJWKVUhK1MYWK4vIFZyI0ewlWC0ZGzCwgMriBVPKA22Fktzxo0MvuHEiYUui8RCclASp6MLSB9/ZROLxGn+mGABbUjWnJ8Lb3jEz4DNXitkuwHT6DG15InDIlYJiIV4oNLptTmJFWyZ3Q9sRrOwwlpgP0KKWPCDFvKBJCtuvgUgm1hoNBaV/Sxd2o9Wd5Zn725Ai/k+J+QQVMi3YggnvCdwA6ON1NbkSCwCrGe7SGPVPCmViyEWiaNbfbldyG2800jsSsC4zkks31NAXbPGzhmn+RJE7MX1efSpR2Ih0aK0Lqv03q7ZxAp2QjPr/1h1qObkMn5qdwNtj4zhTkpCTL7VFcQVth2kQaHDRTfFsonF4ySCsqYzYPOhhYlEXyc3GiDtKE8yxk3AYNs+2brDUEBcWlwi+rCuqqBno+Aq3fJAMxe5kl/M1oDitx5Au2pPlSFTi9nKIQiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAjCHw0+bGhe9afY+s4++RI6reJDgNXUGNXDwBDGVvzbZslkdCGO3Yu+uUsIc6qexYYhlIfQq3RlfiGEKUuy8vki5by/KrFobDzRDgthoQX4W7ta6eA/+qqCNzxn9yxRYvEVGN8Erli6ylmvWoKzeJwSwmN/bbmFC+LPE5dlfP8UwoshjA+hfSbyXpNng0LAJT4+u93JAd59/xnCEyE8mxEVfMHDh+v/CmEM7hUt5hhLZ1EIL1sKl+UvBt80/iOERziFIEGsjiE8alkckUmflOPHfnzAtbUF7sxI3FaJBPe3lqMwh2Vi+AR2pJWWjwMbFewnnlqfRCR+IyZYD8EFeYHzMEZzOIC9yMOJwhwUwiSLPMQuN7LEZ4YwN5NR02WBWIhoTurYzEjwn0xkJ5yqh/Au7pNNgLc12fYdrkRCaBHCghDch8vQECbbryeHMMvkRxvj4schXGmBDnnKwJ3zjNAdjCVOLJ79MoS9LHKmESVYgw21QGsrgDvH3cUS/41jDTIJotO/5gt6zi8J4T2jFHg6hBGW7ChLJx82sNQeN+I6lre8jrT3PdeqKB/+F8KNlvsk6xKuFr6wBOkkn9nrNLLLu0J4yQKdSypol15igbWsqY4zqyX6K+ofwttmIzs6WCXWsZiPrb7Ah0Yg16q0xE6Zm9+pTBW2t7z8Q8H9MsSCwa9kUnvMCBeMK+9bYICxPIkksSjGAxV/Xd2y6G2p8YHsz3wSWLBIoxLEqmPkuNsEZ+uCT/3PCAT4WPsDC1zHh7KZX5PhwcuaKoRPM0K4wuRHbGwn1uSKNg1q67UQnjch740019qjf+avTdHE2t5oUauijYW3rqmJ1A62yBXMImxrWe+bn1gjTSsl0cpeZ2AiwfpFE8vF2FATQgjCfkUQC6033QK4G7gp8+vwEB5cZolF3V1sgd0KEqupaZbWNiyPeMjoGExD9beO7vh3ZcQitZ9C2N3C52SI1cU0YOOMrdY60eSjrMx18xPrCGvXMpO4DCyaWWHmmGZ0Jdu7smpJEquh9bdaGbr/vSrEwlB704pR13TfaZk7z1/WiNXNRAI66BKrkQkWuchI5n8vW8zKpvh+MANrtlkeweytV01ufWvdtFaClIsSz+YEFf2jpfm3hPGOSfRVCP8N4S2TGdGOLjcjOmJBonjl1oS1TXUutJJMzHSPTlbU2RZ/fP6SDK+Y2gnWeSabNkQLf2LjjOKJVdcGHPMypno0LbYwk8uzaLHsTFFWOk7pY0K+sf0hWn5JyKcmRrtqoCzXg/UqCsXFf5E1E61b1eI1S1iZVUL9zDhDqAQ9rQsOs/79nM0pCEJpgADfx8RVO9WFIAiCIAhC9dDLJpNSYDZoiKpGWByMqDh15Lh2Ce8tEWoqtrWZApZlOJB+3UxkT9u2MMEm34Mto46zSdTpmdX4JNueUCUKKSxndOlrGzn62e6OYGvAM2x9fg+b9W5jM4qdM3tafDU+oq8xUhDSmGnSqHdik9MYW/LzPQVTbSOA445cqhCRdroqUchGc9s3/JAttw22mEdMD8YtAB0LEquR7bQUhApgKe3EzBofewqeskB/s8fr2TruWZktcsE2+mUT63B7UBAqYBWz0OfbHstPM7tWVrJtIfNtC8DkxBbevS3GV+MjppiVpqO3hRyob1tGU0fSsMtg1SKerV3dnQKCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCUAwaNJAzeqHEaNiwYejatasqQigtevToEaZNm9a4cWPVhVAqNGnSZPbs2aG8vHzWrFndu3cvKytTpQiLAyiErIJVkOr/sUwGfvJ+Tp4AAAAASUVORK5CYII=", - "description": "Allows to emulate remote shell. Requires custom implementation on the target device to work properly.", - "descriptor": { - "type": "rpc", - "sizeX": 9.5, - "sizeY": 5.5, - "resources": [], - "templateHtml": "
", - "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n", - "controllerScript": "var requestTimeout = 500;\nvar commandStatusPollingInterval = 200;\n\nvar welcome = 'Welcome to ThingsBoard RPC remote shell.\\n';\n\nvar terminal, rpcEnabled, simulated, deviceName, cwd;\nvar commandExecuting = false;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n rpcEnabled = subscription.rpcEnabled;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n } else {\n deviceName = 'Simulated';\n simulated = true;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n \n terminal = $('#device-terminal', self.ctx.$container).terminal(\n function (command) {\n if (command && command.trim().length) {\n try {\n if (simulated) {\n this.echo(command);\n } else {\n sendCommand(this, command);\n }\n } catch(e) {\n this.error(e + '');\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: false,\n enabled: rpcEnabled,\n prompt: rpcEnabled ? currentPrompt : '',\n name: 'shell',\n pauseEvents: false,\n keydown: function (e, term) {\n if ((e.which == 67 || e.which == 68) && e.ctrlKey) { // CTRL+C || CTRL+D\n if (commandExecuting) {\n terminateCommand(term);\n return false;\n }\n }\n },\n onInit: initTerm\n }\n );\n \n};\n\nfunction initTerm(terminal) {\n terminal.echo(welcome);\n if (!rpcEnabled) {\n terminal.error('Target device is not set!\\n');\n } else {\n terminal.echo('Current target device for RPC terminal: [[b;#fff;]' + deviceName + ']\\n');\n if (!simulated) {\n terminal.pause();\n getTermInfo(terminal,\n function (remoteTermInfo) {\n if (remoteTermInfo) {\n terminal.echo('Remote platform info:');\n terminal.echo('OS: [[b;#fff;]' + remoteTermInfo.platform + ']');\n if (remoteTermInfo.release) {\n terminal.echo('OS release: [[b;#fff;]' + remoteTermInfo.release + ']');\n }\n terminal.echo('\\r');\n } else {\n terminal.echo('[[;#f00;]Unable to get remote platform info.\\nDevice is not responding.]\\n');\n }\n terminal.resume();\n });\n }\n }\n}\n\nfunction currentPrompt(callback) {\n if (cwd) {\n callback('[[b;#2196f3;]' + deviceName +']: [[b;#8bc34a;]' + cwd +']> ');\n } else {\n callback('[[b;#8bc34a;]' + deviceName +']> ');\n }\n}\n\nfunction getTermInfo(terminal, callback) {\n self.ctx.controlApi.sendTwoWayCommand('getTermInfo', null, requestTimeout).subscribe(\n function (termInfo) {\n cwd = termInfo.cwd;\n if (callback) {\n callback(termInfo);\n } \n },\n function () {\n if (callback) {\n callback(null);\n }\n }\n );\n}\n\nfunction sendCommand(terminal, command) {\n terminal.pause();\n var sendCommandRequest = {\n command: command,\n cwd: cwd\n };\n self.ctx.controlApi.sendTwoWayCommand('sendCommand', sendCommandRequest, requestTimeout).subscribe(\n function (responseBody) {\n if (responseBody && responseBody.ok) {\n commandExecuting = true;\n setTimeout( pollCommandStatus.bind(null,terminal), commandStatusPollingInterval );\n } else {\n var error = responseBody ? responseBody.error : 'Unhandled error.';\n terminal.error(error);\n terminal.resume();\n }\n },\n function () {\n onRpcError(terminal);\n }\n );\n}\n\nfunction terminateCommand(terminal) {\n self.ctx.controlApi.sendTwoWayCommand('terminateCommand', null, requestTimeout).subscribe(\n function (responseBody) {\n if (!responseBody.ok) {\n commandExecuting = false;\n terminal.error(responseBody.error);\n terminal.resume();\n } \n },\n function () {\n onRpcError(terminal);\n }\n ); \n}\n\nfunction onRpcError(terminal) {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.resume();\n}\n\nfunction pollCommandStatus(terminal) {\n self.ctx.controlApi.sendTwoWayCommand('getCommandStatus', null, requestTimeout).subscribe(\n function (commandStatusResponse) {\n for (var i=0;i", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-knob-control-widget-settings", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"maxValue\":100,\"initialValue\":50,\"minValue\":0,\"title\":\"Knob control\",\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\"},\"title\":\"Knob Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" - } - }, - { - "alias": "switch_control", - "name": "Switch Control", - "image": "data:image/png;base64,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", - "description": "Allows to send the RPC call to device when user toggle the switch. Advanced widget settings allow you to configure how to fetch the initial value of the switch.", - "descriptor": { - "type": "rpc", - "sizeX": 4, - "sizeY": 2.5, - "resources": [], - "templateHtml": "", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-switch-control-widget-settings", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":false,\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\",\"showOnOffLabels\":true,\"title\":\"Switch control\"},\"title\":\"Switch Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" - } - }, - { - "alias": "round_switch", - "name": "Round switch", - "image": "data:image/png;base64,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", - "description": "Allows to send the RPC call to device when user toggle the switch. Advanced widget settings allow you to configure how to fetch the initial value of the switch.", - "descriptor": { - "type": "rpc", - "sizeX": 2.5, - "sizeY": 2, - "resources": [], - "templateHtml": "", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-round-switch-widget-settings", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":false,\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\",\"title\":\"Round switch\",\"retrieveValueMethod\":\"rpc\",\"valueKey\":\"value\",\"parseValueFunction\":\"return data ? true : false;\",\"convertValueFunction\":\"return value;\"},\"title\":\"Round switch\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" - } - }, - { - "alias": "led_indicator", - "name": "Led indicator", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAABCoklEQVR42u19aZBkV3XmPS8zq6qr90ZILQkEaEXsm0ACg9hNBCZmBsZAYMcYRxA22GGH/csRjvAP//QPhyPswI7xTBgCM8yAPfaETdh4AAMDQgIEQmKxQGhF+95bdVVl5jtz7j3rfZlZ3S1R1ZXtTpWqX77KfPnyvu+d5TsbIGI6+zj7+Fk/mrNLcPZxFlhnH3Pz6J/B3+2GG25YXV196Utfun///sm/Hj16dOfOnQDwFI58/fXXr62tvexlL9u3bx89PX78+GAw6Pf7Z/G0TYH1wAMP/M3f/A1d7N/6rd96+kf70pe+9MQTTzzrWc+aBNYXv/jFf/mXf3n961//rne96ykc+V//9V8PHTp00UUXEbDonP/sz/5s9+7dv/d7v9c0ZzXAtgQWiYF77733qUmRU3qMx2P7/TQfbduSA0S/n/6hvlEez3/+89/+9refBdZcPujKXX311SRmnv6hLrzwwj/4gz8gVfj0xdXhw4fpvjr33HPPSqzT8LjvvvtuuummJ598ktTQK1/5yvPPP9/+RBYVWT+km0j3vfzlL9/gILfeeusdd9xBuuxFL3oRa0YSlm984xu///3v33777YuLi7T/8ssvj0f++te//uCDD9KRX/3qV8dD0ZnQnxYWFt761rfazn8rDzrmOeecc9VVV7Epxg/Szt/+9rcffvjhpaWl5zznOXSehMjhcPj5z3/+rrvu4i/4T//0T/TVzjvvPHq6srJy44033n///bT9vOc9j/abMUf76TgveclL6I333HPPW97yFn7LWWCd8uOrX/3qZz/7WePevva1r33gAx+gleVr/9GPfvShhx6yP21wHELVl7/85de85jUMrOuuu45s+R/96EcESjP8f+mXfokMf7bN6ch0Cc1yjzr0yJEjdKjl5WUGFp3bpz/96e985zv2gq985Ssf/OAHL730Utqmj/jEJz5BMOr1enQQ+hQ62oc//OHRaEQH4dc/VB4EekIJgexjH/sYSTL+Ex2WQPxrv/Zr5HbQ0+9973sE35tvvpnASk8Ni2eBdWqPn/70p4QqUjoEpuc+97m0rP+7PMgoIYFBUoeux969e9///veTnCBj5Qtf+MIpHZ+O/Lu/+7s7duygT7nlllsIbQwsOjKhiqTO+973PjoyQYGM91kHIWlEl3/Xrl10kiRN6TQ+97nPEdR+//d/n2zHv/3bvyVU/cIv/AL5DSTq/vzP/5wkzQ9+8AO6N/7wD/+QvA2CF33ou9/9bvpGZLd96lOfIlQRYn7+53+e7hzybGgR/v7v//6Xf/mXI7KvvfZaOrHtg6o547HoipI8uOaaa17wgheQhCB5Q6qExAlrENJi9Psd73jHJZdcQvAiK4p+n9Lx6XoTFAhAdNXp6aOPPsr7O0emjQ2Ms29961v0+81vfjOJKJIrtHHxxReTfCKhQif/K7/yK7/927/9ute9jkBGWvWyyy6jF5OGpacEaEI2PaUX0zb9/slPfvLII48QRt/znvfQWR08eJDuGXol3VHHjh2zTyQN/s53vpNWIyrcsxLr1Kwr+n3nnXd+8pOfNGuXfj/++ON0c5MAoG2Cmt80p2hN2+tJWtBvEi3s9PGRSUaezJFZYxLHYXtI00VL/7bbbiMpSzgjC4xVGOnBqYdiu4p0ohlVz3zmM/fs2UNMB6ls1q30oHvsrPH+tB6kC/j+pvvYdtLNvb6+TvYKO/xkEf9sP5SuOh+ZLPqTJzJY9kw+CFLf/OY3SQix0P3xj38cZU/nQd9r8hsx6Gdh8SywnsqDrgfd4kRpktif/CvpHTLASbrQy36GH0oXko5Ml5/kBJvMGz9IS9INQKKUhBPvoTcS2kjS0E5CFem43/md36GnfKuwWJr64NeQPI6oZfF5qlr+rI210eOKK65gd49vZTaDyOXmbdY+5GcZC7CBMDhVpioemcBNFMCsF7OGIhOeXVcyAf/kT/7kj/7oj8jE5vMhYcbKi1BiPmzUsKQi+SnxHbTn7rvvJnKL9xAuSUGzvXVWYp3ygy7Jn/7pn8Y9xBmS3Uo2NTlcdDH++I//mDxBugBkVpOeomtJa002LDnztPSEtgMHDpAfbvh7mg86MhlGBCxSwWRx05HZ/Jr14u9+97vkVJL4JFeAXkwopKgivZFEKckzQthf/MVfkOVEtjkDy5BEnh39Jv34mc985lWvehVZ/a997WvpRvrLv/zLF7/4xfx9yXgnJ2MLghNnpsS6t36wRUzm1Ec+8hFinki5kIdIICNj9kMf+hB7Q3QZiA4gYUAcFf3pyiuvpD0/k5Mh4L73ve+lTyfulI78whe+MBrynQcB6Nd//dcJN3QaRFgQjMgH/MVf/EWWVURo0U1ClAH9iQ5INIE5JfSgr0afRaglMcx2Pen9t73tbexsEljpmxK7xrzdNn/APCb60b1LF4wuzKTRQ/qFLgntp7/+bD/0VI9MEotMKILCZNYDGVu0c5Y3x64uG1j8IKlMH024JIG3/WXVHAPr7GP7P86meZx9/LunG56KHzDNNdjoBXASB4KT+rDuqyZUGJzRK39mqkKcDSacvLYY3/jUVwMiVCaODLNBBmeBNb/yCTvXELswsu2NJdosoWNQifCS7fpD4d+HDDtDgIUng6caTLxRXoh2maeuBk5RmFMwIP6aHEGe2SsrkG2IMDgLrG0HqRl4mg4m8O9uL8EAsknxlWaZTeVlBh0HE70g/2kayE6EMDgLrG0KqQAUFEmEJpYw6X9y7fPruvDa4CLjNAFW8GKYosOCQAiCMMub/HqXYWcivOYSWDMhVYuoiCfdKv+B6DxkjJU3IvqxKkk22yCvJJP8uWyK1APBGW2gi6koxhxheuwzBl7zByw8FUih7s7PC7wMTCybWgNcEjVpb4HZVpeBCVksJYOMA67Jz3QPg4zFGCMqAPFk4AVngbWVgmompCDoO90u2Mr/tPwn+u5hJ0rqVU7swvJoc2EXb08xrggRDVDyQQPl0cubPQrUgMikpADKyOMX2U4IMkz3z4bXfIquuQEWTkJKDZQAqa5Yart4UikFiX4TksbtmH4PR6MMpAICNfDlxRsaWQCuB/Mbm4K3XBPd7/cKzpqCG5NeEWGNirlpwgy6tte8ia45ANapQUqkUWKxhHmj5Q3+Ez1dHxKQRuvD9dYxJC/gN5ZL3/UAplrrLFdQoCNWulnrBJ2FwQLBa4GqDklssawqLyhPeUOQBHBGwWtbA+uEuq+j+AQ9si0iKosqSCSZCE8Epiyc1LQyzAk6zWE0RKWEU0kHzzFwu8pAw+fI6GFji7YHGV4LiwsLGVL5LmhA/2SoclCqcpxfzbh9gTXTSFdB1TISVGIJLIrYYnE1zrghy4nQNFxdX2P1x0KLJRkLubYgTxApDqPgNdrvUSn6fjOPitkk+MgCrGHx0wiAGhFOmBYLvPr9Ae3v6f7yLsWWuZNBnpl0nBejfpsCa7r6c9Hiui/jwxVfhlSr0BmOhqtra6PxqC32E/9mkytb6MASK4s0OlRrskpNNJxG2XcIdIhGkqm2rM4UT5jIrjejKtv7/BuaJSrPWFjklzUMr4JSBpNBU3E2Iai2N7a2HbBmqr9JQVUEjDhuKUopXFtfX11bpe0MJt0veEqCPLow4wLKsR4hKEHViUpsduks3R8gpQqx7OkVJZh/F6HVlP2MMJNSvH/H0g4SYPmvJr3KEVRjbii6trFa3F7A2hhV0aJSYePyiQXS2vpahJQJKtkIr2yTWWMiuuhdD7aHHmyPPIKHn8CjR8arwzQetu0I2nUcjsu59MgeJ3sJG+oBMki93b2lZ8Cuc2DPec3ug83enis+sZayKFJTnQWV7WkUXvQughdZYNUr1WhT0VVbXdseW9sIWBt7f7Uh5YKK5Q2hhBTfyuoqGeljHNOucSpYaceCPNDtGoiH2+M/Gj14e/vIg+NDx3CttGRA4F9QWXhTuCw+SxDynhCxC5bOh30X959xZf/gLtjRAUqThVaPLTDZzkjNVFgPevR0eWlp0B80KvMmRZerxW3vLW4XYOEJ1R+4xxcMKSzCCY+vHl8drrGUEkhhywgrYGpLPWvBE72lHd86fvAHo/vuGz9xCFflwwrLIEYUonPj2JELlr4Qoj7CT7BAFXmyt9lxIex70cKFVzQHCTQN81ipyVRqEnGV8VSQx/AiDC32F5Z3LDdgmjGYX8qBbawW4SywTglVKqKS4QkzhpAYhJXjK+MiflgDZkgl14NlWwD38PjIt0d3/3B0/zHCkwQIRctINJovGe8xinSyfgHR/EM+RkAY+p5ylCVcuGzh3KsHl57f7DEAlQ2FV8rakHViU4wtwhZpxl4QXaJey3vnAlvbCVjTjSolFJJgq8AFWRqtrK4QlVAgNR6r6zcq2GLwEZjo93o7umF8x83rdz86PuYJLJhU1ICAIOIpQUUwKFiSW/T2V6wQ5v8YfyrvP9DsfPnCRVf3L1lo+r2CLQYN/e6r+ZX3Q68wq4PlJRZdvabYaqIWxUXoqsWIrbPA6oor3MCoap0yyOz5aEjqb5QtKpVJCUcssQqY+Gd9PPzG+M7r1+4oIsrjvoxWF1SSlSACzC8VnODUnaBXEeX5OWhkWJU6sQMWXjG46A2Lly/BQoFR01MB1lcygjHXb3rZqO8PnKRIOTw5y+SC7SS0TjOwTgpVwagquMmu37HVlVbRUyDFXUHy0wyv1B7H4ZfW/+27w3vXcAR27cFETAkD85UAIQ5YygCamQVmyk+ppuD3o4g6BFGFmuOlAlgAlyzniz9nEfqv6D/7DYtXLkOfTK6+6MFsgfWLxGLAEZh2Li0T3dVT9qsyubYxtk4nsE4SVeNgpJPKW1k9vjZcN0jRHhZUIxTpRQj7zuieL6z9cCWtE+8pK+9iKehCVhyI7AOCgA8KgQ8MxKAE63MX9GRIiQyUTK9yxJDZp1mpqRZmmeAi6fW6wcU/N7ic8CRSKis+xpnDi8j6nWTRi//YGOm1nbF12oDVRVXNqk+gSoyqY8dX1kfrWQO6vhuLlAKk/fePn/zs+s33j59gKQGGJqxS8BKKqJHQnkLKLSdU2bZBsVcdqxYUosg4NGlmOdCgSEtgzD7tO9jse9fiSy7sHSDd1yD0RT/2TEvS/kFvsGt5pzqSMBNbHQr39GHrdAMrGOwxljINVe3RlWNkWhUYZUaKYMRgyrIqtavj4eeG3/vu8J5s4YsoquhDCLKqQpWZvahOYLDUNwaWIMgwCBUJ59iybByovjqKUkWCy0sHz37H4EVLBKGsGXsCssJT9AvIyNgiuUXMxUbYAtgmhvzpAdZUVKVkwbsuqoh5yqgaj8ai73AkgmrM8HpofPh/Hf/GY3jUaANRgrrEINb6VFQF9LlLp+4jdllSN70w1WbWRNyggy1m4yCE1YtCVJ4DD8Cu9y1ddX5/H0OK8ZR/q7e40OuT3JqFLU6m2CbYOg3AmmVatS6xHFVkhhOlfmTl2Kjl1nrZispSqvzO2zj+9vqdnxv+cL0d5wwBdH8ODGT5MxtlyCWy66hqk1jsAVKAxhuobqlOHp2jgBpeDLEmVRn05hnmg7f6HDBwGyXDAgbQvH3hBa9euLjgKSvBfvmd3ca8QT/93cs7C0HfTGBL7PrtYGxtNbA2QFXSdINW/DuRVYaqsaBqxEbVEEfrOP6H9ZtuXr+X3TKxYjiDpY53RMkEwbWrUJUi8RTJLpjgR1EFXHydcRAT2KolmeemFgWJDmip7Hnh4IL/tPCKxYZya/pschGesnUfsGVyKxv1jQSOmDvdDtg6TcAyV9zcwBRRJYRn1oDHV4bjoaAK2ZzCYcbZmEJ7Hz9+3f3jQ+WKiOyI4kpAFlECopUkGog1qrIydjtMEwtghi70II74fU3kt8TT1EiixKQMi6hCrhJacqb5qOc1e3915+t2p8VsuWdUsaXFYqwhW353tuUbI1QNWyqqwfT4aVGIWwqsSXEVDXbJl8qxPPH4jhw7MmzH6gNmVLEGJGA9no59fOW6J8Yr7O+BKBPxwaqiUVdRYlp5NXzyV4rlwwAVGddJnBGJpFl+piFRkrwAza3UeJSJv+gkJhQsmtBCZSyECePiH4o2fnDH689plglYohMdW4S23q7lXVlLQma/IKRRdA350yG0tg5YJzStPFpc6KijK4VZKOb5OHt/I0PVQ3j4r45el8l0lypOmk8RVzBNCWJlvVeogmz7O6tZXxJLu5AIYaO81BRsCSPfUYhixXeEFtqLzbfE5WbpgzuvuQD2ObZIeqlpv9Bf2LW8zKy9xba3ibG15cCqlaCYVq1SoBzgw/bY8WPMghKMyOkbFqFF20Mc3zl8+K9Xv7FG5CcaKIDNdvbqmmRyQSO1WKOKnUSnRMt7haLQKLSKQCe3Us29y+3huRC6R96WrDQWYrmixSHRIZYtPbTUQfRaoXxmi2nhv+x4zXP75w6AdSKlgmUmYlACjoU73dkLAWwoajEaW6dFITZbLa4qjkerHzwbHUv+5xqjKvNVkIYlxSqjqh09OHryr1dvWEtDkJKqFLMuc4kCS5yG86NSflmSpGB/aFoA2/gFoA2/u2yJua6p53yM5D+exidYlL9bUnHBqDkQXitRwsil7AvKQRPXW+QnaAd0YVP+aejLfmL1hntHj/EKlKXIy8KECy0U5XfY0hUOT8q8OzmxkxfiTADWpBJMWrsniZ3AsmpMK7WydtxE14hNK8xK8Alc+fjq9WtpFIwk5zHRBJgHkoM1z5nGGMtqYu0yqsHONECQbjm8Y1AoMEW1yOUFoBwFQvLKsdQpeGabCjEo0eBPmryqiHx5P4U7P7l6wxNppbDBsiYW1KKwaeGN8x46F0uU9cpImFFrNO/A6jaeciWoLRUQuf6Y/qfEYsm4Sq4ZR2l8NK1//PjXDuNxy2NRm0TjfR6HQedFFb/mK6otpI6aVVyBeozmP7ITUGqd9Xso+9AwwoQjAEOsSh29fxDQP9SZeVOyujJO3QsaAGLlGaTDuPbfj/6/o80aLYXHSVPLRUeU6MEl27yMRp61Wg6eOoz0GSaxUM2LSSVYkJTzq4ZkrHMCTFk4DtestaO/OvbVkk0F5k51VwgrA0IMC4RorefXtMxdFRC0hV8osRW+CNCKQZ2Nd1aBtNGWQLP+yB5Wikp2QnFo89Oiiuiw+VAstNryoQbLFE6s0/IPp92RSkI8icc/duRrq+N1WxZbKFo0WrpxCgWVHYUIaYt58GbrxZWEybpKMDPsVE2qUirbVZxuRYv4d6vffgAPxYtQhWNwwoJQU7jSg6qeIES8tcwGgwpSVBlhmaz+JnnDIglIKraCWydCC4NHaWo3akMVThMnXzdrC7fL/XjoH9dukXBWMT0tw3GYGwUMeSVrhdj1SLdGaDWnR1yB9UAT6U2/j5fqGklbKDFmpq9uGt5zy+i+TkwvTdtMUDs/MOHKpTq6DBUDDiElBjRVQfkL9+fM5xNsAaSY5YABHdCpc51+PnVXtslXQvyXMoK+vX73uEisskSeKHs8WxG1QtSl3nqh1WyduIIgrtDrlbnAgTzBUY4xawpoKwb7Q+ND/7B6E2p3jxAOgWScthnTtXDsGlgmMECpBhNRoLntykSICdcYUw+mkoSBaMBeBuhErDcvQsvGUYymCTMrdVl9nPhqvohikLf/uHbz/aMn2JAfl9uPcx5pAbkhBfOx0jQHA9UBWye0tpRu6IorTg0tSpBqbLL1wuKqZaIBV9v1/5Epq5OZNb8hQQOzpAFAp/7GuAZhkTQnBjtJC5yDjBAdwAAQP8YUEQtP/YuUx3oafXrtW+Qq5qyhkjpbtGG2+46vr0rltzehCELrjKIbuuLKAjhqaSZcXV8v2aGiBMcl3YruxX9e/97D48Nq6xsybZXAn5okk219izHbXP1lkqDVKo3Ep4Kg2/qn5FxmEqpChRxCJ45Q3uUxqjZUU9cfWgxuBPDT83gPBtMP0cVV/I7KWVCa0D+v3sKVkkUt6upli2KtTZ22KBiph7Ql1EOz6XqwK64kFlYMzLwcuQPMeFhChGMrs6EbkYr+vrV+1yncyLMwPfsAG1hqyTPbIRjgddOGE799w8889asbj/mt4V33kULUzGy+FTF7iEO24lttkeLLXgstnG+J1XEGw4NvLCGOAVu1Q9tSFfgP67dwb4Wzj6kPgtT/Wb+5lJmI8d6WZaTFJC4QrYfApKU116oQa6o9WlfFNhHrKvdAa0u9MjojSr+/uX7HXcNH0tOMRQBsfADcKOakwTrvkuUt2LwK5wRv3/AzT32OV+eY94wevXF4ly2alOkWPZCJGxNaU93DTTbht5BuqMUVf0lyBttSB8Epo+zdHG/X/u/aD2LTYQsLKrPNdBTaUzBvzFuZKe3Er8qxf08P1+3ytNHdHGC0lxn3YLa4fnJg8vVojeZZgGwLQ9b5UObxEYwYs19geRke07Q+SSHECZ7SQY/PHf/eSrs+FtaqIKyQWLSwOLHkWxnU2XxgebE81s5g5vTYFOC6rpIymu+8L67eegzXndJ0Ozf5duepG+/h7ranLYY/VYYzYvgTFFLeHCoBUPd6aPUpB5olRzG1oU0IWrfvaafXYvf0qjPHk/q+SrceS+tfXfuRsspSy5Tbq1J+URZaWLmHCbGm7OYMWJUe1HXmL+csS+lixTRM8ZCRt9fa9RuHd0wK/pjPCd4ZwfnJGZSshhMx7EFNaQH33mp+ydJpLDuq8ja15FnfBk7kI7ojLJ+LXh7rNRcAuMH6ha9mHb3TlCTpfIzrR3dwnGdcjIpMB5ZtWt42OVNYI2vTteEW0A3+D2prq9KxeEhLwLnIuceQJmN9de22FRhN/754MtZRVX41PfQGikbLLIBoP1kvb++RjR4HqnYqFMHtMAgZFmjqeKPzmdJBd+qgqBm3zwquf339NltAlPy2lBE2ZqElhEowHedXFaIn10WWQdhRSicaj8Wuko4MJWdmvP710R2VYz31pkK/ilUzGE9IxlCAisacpxjst7JSqa8PnYka4CY3mkOcoHb4WTpJw+8m9KrSlHVM8esL+YQaNPTWNKJOq5hg7NmMs7wOrE7outEd6+2w8H+tZ3gX38gWPPIO4ZtgmjuJVelBM9g1B4tSiDhzEovKYCrvG6O7joxXjBI0S4VrDvMla9uYIyi5CW14vdox/GmgOTrZExcRU15R3sKZo1aU411sW5ZQY+Y6vRkz2jkVrU4hARADzu0ueyLErOof4JNhI1tcNGVQfZn867TJv2YqXxwwtRhGaDiVeni88o3hXfK9eT3LZwwLC4+xsSpipQ3n1XivGEqQ+CBi7j1k3RxLwIvJmBvX76i+dLRtodJlVhnPv1BjepV4CUICLd8mFNwnkRi67DGq54dusbaxFKrJAnlmeCVvNA9uItnJi3qFbuYduPEnJpXWQCaY+OI4TZNiunHtDqYAi6UlC1vCZSNb9rp/3Px6hRAX3fRg3sPWOt9YQuWl9MD40EPt4fCtIazjVMoYQ7891JrlJFkHmOIezzZ1cKXgVCQMVYJWCG84LWKgcEJW/GMeCmirNozeWyS6g66EoMgAFUlhj1nulT85YWphuLXK4wF8kirh2E6XJS16gFh4TFVNlN88MHfACv6gWQ2mq5hokMocpRsKKXonWmplqCrBmDYCsd6hih3rZUZQUhMBrFtCRBCqkOPPwiZp9pQVUod4noIWYrgQlJizmn2BUjlUo61IxCfF6E8mK71QSJVTRs+9qbL/dAFDWg1C3fBdqYcbh3e2SuVYj1YmHdxQsKrKDcYjzIVXiGoem0AuziBaBF5SPtrR90vSVZXSlGqRI0pCLjZ2jPA0TRtaAaDyjy5m5NWWcmXY0h5WwEkMiBWbpBfJMxo4w1pQJeQb1kxEoFMtKWaKHox5XxZPh1DVGAVeVZmYH7cM7+Uy8VbYZjHo8oLr4uuohLkmSENXlRT8QRLOrWRki1dIO388evhQu+J+FNaZpzkGZHa61CvLfmvOh0EsWDfcpKlXrcQm5e2tiidkL859JDN8OE4ebg+Q+AjvYV2uREOlhn3OJgh3qokcJcvZqxu4rUBgjrXQHkOPkVb9iRJe7aTxhLQIoA7QPxk9kszA0MEIw5H7hikywjiPwApEg2uBcn0zz6DeEdsE9EPth0Jkw/LlmhRCNhLVKIERLYCQqA4LEImD+CZY1bKVViXtEcvlpQihorBRekuNHykly+0RSi+GhvPgmzAozm11LuER5aedAblhRyxSwzgm079D0oRmi+BIemrp42eRnjBhJZ8Nb6DFsm4Z3dMmDLl+Jc4zHreIKUWrDzbbzNpE5r3KMFJlMWb6SlOypEs7tnePH+uEeYOtbfSz6YiONhQFiJaKjjKjMpjtqFrSCuSzMST2DXrMGUwtil3YTjKbyHeFcFwYes5ojz9U0w3D+VlyPSQrZLWOuqZEUxSe4A1IVEbX06Qh+jZ41/gxtq4kbYaTv7FFjEOHsMMGzp+NpV48GIkyzs2GwFxvFtFH2tXHxkerYgJMRh568iZoZp+TDmbHpFD/ZZl4UNn3OtoiMvSOLVdl1uBBTqR1pky3zVkEdhgDg26oChOAMaxFuJSA5ppA4MHASV1vwYXoJWGRBMYYFUgPj47QYsa15bhi9g0x5GZtvpnVbLKBVdO7kEQsQwoDcBLNhuhmURbLUyySZKZVC1UPIJ0PJ5Sj5sVnLlQvRjZrWglEO3eaqgGGrLqMHRUTTU6ILxCgDLWAQrSCJ3omM5LQvNLkyZtWlavytZxM24rJKFK11dBQG86w9QZbDLy21Rg2qtVoDK5nz/6IWltYgq6O/MwtfKbn2M9rdoNTJqg1OUk7bMswHGx/PHwg1suYhexiX5lIDCnOlUQCp88tjKOdFIxGwqrUKvCgUigoZxU7ZQGqTOr8pCpCCOHtrdd2YF2QprWOqOVhHt5xcYVV7AgxhK7tO1fmAaCTurTr9uGDNmzB7pdRzh3xJcMqs2KOgAVd4RWUQWuhK6vSuQcf18Af+mVITnt6zyC1TJRPj1FnDQ16/Z4FWSCwR3UIr1K1xV5C6zQEzjLE3g3OO4DEpJU+67AldcmYKUw5Jc9g94La0C0kOTfhbZOMqlA+Ldwqcr53t4/FKp0EMWcpWFgzLtZcxApD90Sx3Nu2bl/LsVJqc1X1AZX7GPXqQYod17ss7KTQUiu+oozQm8L4W2qWIYscHoqZa5rBQwUw8SPl8FLEnqHTJqy69ZlEsbJUxC6M7aQ64mrGF9RAO4LNVrTiRkPJY+NjY2w7/adztta49WLh6BjOlyrEugMZrwLVKUGyYgr2vfHh9lCLndSTYqeAUfBVHjCmSmj5JXCGCGJ3f3QtYTyj81adWI8qJYnesEwAxpn9MFeucR75QnXSMVpb9zofLEgfBT2EJwgOviCusM6WtrRWaRrhYUgJPz/SHi62vpiO3H6EeTxMHtZP08Zd/6we/U22sDDQDWZReoCnhAgPC5fIrGlIZ+MXNCGK0YKHx5xSyCnFGIj4fLlbCPGQgDJrx1aNuQm1yh7wFfJ9ciB07G5SJ/h4VhbGKGGL8RNVaYK1jrR+k5Sxo2aVCVi51ZxvEFZZu4cwenTGSv4rLemF/QMircQEC6Vg2mgeAOZPYrnKAYhsVv7IRmjQtsTraOqkiqloL6C1ZMFpQqtya2JnM2CzPfZj6HRIc/4pZmRaGBAn2KJko8jjRztoEStbzMKaYHxYmNkEwYXTk9S2cfZCnC77kxf2x6nU6JqdNx9tD5eOJ9KaonQnTZHB8vOe17SZKmsm2L3u71Pt5ZHYRRhC6BpTMDfNno/sYlA9YAoR7bJhB1vMUoegiHJSlRpmdRc0Ywqsd+hJqvQFoOcZ66yTkNyOUvWRJlBlQBdXGFKlmo3jjeLKUifCeXfGEqSH01FjG/w8iayBretCujmqcEbKJw8G90Sasn24XbHJx0WQe/dMMZIbN5EQw4h3mRuP1mUUfDQqA68p7l3j/Q9a761QFEITKNkwCDOkUXRaWbmFBB3/VbodM5UgHyP6Bkypqxxsymsbi4Fj6FTJ54VOJ2gogADc6HdtDXCNrGjjubRPjI5iFTrFwLNPuTab4RhuSYm93uReuyKhrfzPehqHXjuW0hGElo4Xt36aGGbAIXYaS4GaLEmZg8B3N15vHgxhtbZDCnFwQWckkEYRYsa7t4jEOECgoKiKAWAIG2KVFh2DDJZdk5Qp83nrcakg2oc0bzaNrKgsMivo7Q5wPiXWDGorEnyWc7bWDqUjHqa6tyFIBgKATyLhP7N1lkHVJJs3UdfV6PiSYLNLd8bApaK8JeSctzzOS/RkzHyqQnYWfAb9kNY8TlVOEF4lkwowTPJxFlV6tyk4bIZFrozwxrfBUAhjnywvwidZw8p4zdrQm1XVtriJtNXWSqyptSZdaUzjT0O6GQZLBJ1usITfDh+kvd3V7zT7CULVTGhwbT6BGbOoNi9qCMeGC0SnLGlDv9D+KIXmRuLVonX1dmfF7Ps44BAreY7a71Jjqymcr0Ug/TBKN1Rprh6RoH64aab5v6HJMo/G+xQeolyWNWiV9E7OYgWxZgaIpQ6I5ooKEbQ6xqtP/G52+rpJIcID7ugpvKQCotO6M6WJytHKe01WyW6QqvpClhytJiVPHMPOSWoCY5gkkDSXUBMwrIESYEx99hCYTEMo3RxseeNAqK28wpvMY53EPTGkomd3AK0ogpODdeIpoqeAInqDdhvDxGay8VRmlUnnWaMswDu1aVfuWMqarHNycr5plm53M0x5Ay9nBQ/KobtumpHqdpXNk0b5kogVlZpSqOkX8gvBg0P61Dva0jGmNlPpuIRYZzjPr40V7feOMZxJYdN5FobhW8+63yM3kBXXqbU4s83c1SZ8iDJVFfVyKba4VgvjJK42eT/38jcTnSEwPFvPo+eH2ahVzf+LNhFKJ10wEcxjA8TGK6kHiFUnvyrDRwPNqqhbAVmjYt1y9Vl+Nk2stwtlFFv32FxVeDL3BE1Hdj4mzuX2Pokg6sxLTKuZk6GtYph+iIHgRjd5tBJMRJ/mabXifjXJUnsxsFnTf4wEL2/UtqWtXGqz9kAL8vU0pOdDcDaw6qQZTKsEnpID2qY3mFXO0oO70v2TuKywyVb8abKxtH8M/V5oBjbSqJMpBCFwgt6aU4MY1hYdrRtfF1tGQWCMVOvgOaNZnQs1hCnINvpprFrf8OTcvZwgoJU+plBTpsZRB1Wtfx3NxEGfLRzpiQRTct7E+urZ8Iz0VJolbX9VWH130OGfHZHcbyGiSLgFT9UtUS7VMOYJceiXZ+ioTrSxl6F00LsniOtuCtca2UrQDD3ZC1qs23zDRg6vV5l62BsAfWKvabbGyiVsOHRHVtmkVgwZM2KcJcuHyDQLmO0ujDB6J90F6E1SoDCFdz8TbCyJEqq/4t1jlmAQAhhhdm4coNsq+cw2E8/c4hpSmVMKbIGJHcyWsNFinubrEkAmmbgjByH9PIIHNrpnYoC7DTa9eXIxuIhGo3ulYWVZ+lgn8BpEM/0sQBOsuDC6DmwK3g5cCF235NM5YrhlLf36W6D1ZLqkmdcQOhshDugcQPSYFiJEc19G7SrgNHmzxSnYwqqG3lIZnE+KZDUqvEJYVr2GMHRulpfuV1eIC4A686Gt5/ymYECpewoqgUNIQlEFhqqqrYR2w+kWiaNxdk1agoWEGAIYqUpn4Jkbc2m8w3RTkabNJhuPJDoK9jdLTHf7O9Gn/iFWNedubUg/GAiEs+XoqHxoADpdHi0maXRj6wlg3mdZ5odN5I528khL8A4gZkawm8EmvSYcxIZYQVDlgGEHVTAVVZ63lSytFkMILJTxlIShtL+3bMtrcc2w+FuhD7eObhCmKZQEc9LAeYN9MPxpDqY2Ji5yYBUa7/uRPA1Qwq46dhBTEyLKcboXWsw6mcDULfAAW+jY0W1ZClWjhY5aB09wrssUwKLKdnjwqWcNhNsGtZGlecANaI01i2QbX4CpHlwNaJNe8yw9aRhQyoMAz2t2e6ViLBLbkD+ZG68Q6g5M/EXzjEa5XaXq9NxmD9a9yTAwDR6n8MS3QK+D3NYhqdRiQ5pfkMQgQ2O3zXxnLj5pENpkjxIDXHVjoRz9keS6hLHZEkg2PTMLjYd8rNpaOI4G0NIV4tAVVXFodFtsHmKxx4pND38NbP7B3j6QIY18ffOyl6HRlUreVH+xv8n2FWi2Wb42jZU129xHTAd7e5kJBWyVntQM0MDRoI06xTjqr+wfa/8Pp1bRHSluPmI4b41d1wwwCJHrEPBFG/A0O6pez41NPpEu8AZCg3uynabBtn4GHkxEuVXsbagD0JOXmznLZ1alEa/0elpSa/MbC8htXHSa0nVyToAFTreApIgXgSUJBkxEl30ksUj9Uzc6bSiFYO07Q3KJ6lInEhxbYGpNRlGaX2htO8CtrFDzYh09AsKQ5Th27e6Z8PLAjI9fTNHCdkEbLCRvlqTDN72jKdYpQCkUgWBQ7KG1ZZJbkjZoGvkzertAXYnSwpnZmEbT1kLnw01TjZupCpWJURMy9Xo2DTtZSiaNMd7fLNv8EhuQjeqZh6HMVbm56URls2rbPTSljTkvGvRQEt8LIcJ4bAw9sGcLY+uZHU7Omqnr+TRe+Jqq3sehN27IwpO40GxUKRKlziNF0qvsfAbspDnkmu6q/R9onXuNdsw1D2Huct5x4q72UUaW4guNdjJ/bu8cvb4QkWBVBhtiS0luD4bU07V9cmCww7lAqrGKPs1oNeJcOlnjRnFPrkVOoXmlT0ZM2EhJ9ASkNA3R03KgjuGEaMJUVCXsmlYaJ6KN5w7OYVHYxKb49nsSTThvEivZ/eG2NAz6fXPMtO9Lc/ngIDt33tJa1QQmT5yZga24OF7PaqLLS34qB8GZSrQEFeU3tVSwOyKpMyQKw4vD24UYBU29qyCVqr4kmLppsDhBzU5BFQbCDkIUuvhElw/Ob2wmgiZtLwwGAJGqg83O+OtvniIMERFjpVJxTpQyZYsS05UL58NRaK1jVbHHpRsxqgmuEVmbnQoxbCtNg9DpUM+VgajkULskYevUQOj1bmZhOsGNHZOTPdU5JApau5g2eQqMJh5qnatEujW6BKErUopd4ysNiKF2cuyoouP1EJ4/OFiGCvstLWVRfliYEXWbo+wGifGZEwh8M5GtRQZ7U1o/0c/uZse5/V3qEWqycBvkFlY6sUZCqOnysAaitz+z5JjQUy2hTcnx9hI6TBUwtGI+wU8LShFYU1AvY4ComQXRbeDKEXzynd5WsZbby4FcA2KQVUIRC1dC/53b7NrVLNnC0iL3Cqz6vb7PTZEeXHPIY0HtcYSeYWS/99x+p6citOF5vfMsVpikiLwItqbGlma5BWyVYzWhCENjrrFKzOBlsRcj9jHYdaHdXcUtVpR7fG6cO4YWOJCqOSralqiNMzt0enRoqCy6TAt8Ql1HR1alUprRJi3Jdhr24v55vNo9aGzUT1hwgGBlTYycnguJhR3HUAQwrRthqyhB7nWXSsM8eMXCsz0OLbpN2/5IdEVif6jZpFYg09bZIx4kg064NpQ0gvqdCVyGWeZ6CmZRx7TCbpYyOphqz0FaE0awmgSSzBwXXUEftcYIKzQwJjKCoSp5TbOa/C9beDavJ4ilkRc538zBe6pcwrlsYwTBflemlH6TWG5EyjTCvye4dOG8/b2dmr/ewZZVaMHUoSHeaxTkCykrDtZZHVNItSuXoU1V/7Rw2tFkRzfS408AWwrKzoPZIDLSG750IcX2vUJKsTBrxI02nzENmKpK+bJcB3o7aRm1lSV3mCx3MkUJdfEThC6/89eOe8LMYveEHWD+nuWb89Duppd/ei/pXyggknwjDDoRq2ZUXlhQmVxtnU2A3iwENe3Nba9g48Qa1NCL5ARr743DoYohhe5EWNlSNaQioYAqqCqjKnyz0LLL7CpGVWOFk+1LFy6iZewVSDHdwBKLDaxGQx1bYGBtXnYDVGaWJehKg9lMOiiV1fCsP7rJXrt8KRf5MpJquQU2aNmSQDwd0Js1elupcK97L5rQXVLHobQmw5K1VEZLhagbYk20ywIfvOJdcaW9pFhvNhNAm3d1C4DAmwkE9ddxAJOPr46o4lBpQRVtX7N0MbfdbdRJKqjqaSTNqdHKwNoclG16O24nRW2wY4LiEnI7YmBtSE8P9vc/a7CfmyKi6cTQqFp1RJsq/yilmijycr4uvALdHQVYgxVbjprPjulEOe8af/Eaaqb1KxHl5nmnogG80WHdCTx5foYAoQ0sqNa0gcsq+vWs/oHz+vsaCEtaVjWLK5/DF8nSOW3HPalJClBYIGeaVLWh4IzYFoSrly5Vu6cNBTYpYktCj2pyYZXNYJGUGfDCIMDCTB6wpqWNz7r10VJpwmAPQ2LlYI2SpuAepX1QxzyvIGWwgtAG0kUvavA0ospmjAmqaM/Vi5c0OT0sUwzSs7woRJJYtuydoRdzmzbjXEOYuatm+9LCYhbUTbEJisQmxuXqwSX7YCe6e6R+otlMoXi+KvVES5lPVV+1Cl7WQMHHMoUcQOcPwe7xIJj8J6AszAx2uMRhTg6oJqDfpJt1ZvP0mSiJ0YL5Hk8ET/CSJUrpAOy6ZulSWsB+YXNoSfsFXgv9AZvwEC4CwOYSDZsMLIAO6RC0IdvszLWI6CbskOG50Bu8aflyiMkrFgps5AqI6OK8FOaGXHSBJTDgFHhBMqu86cgwAxk4ZQon/nEdZm9ErORTo4vhNplDSnNzlL3D0MqL0z9aEVRobE3wXRRn6dqdzx8QqGgJEczRzvDquT/o7GjUgwDzqQpTqlqRiW/YFDarL5Z7zv2DnviGzc8tXUHccUkF1JgMaFe8xil4UYucberTu5InrgR4uXJsmSZ1GysuQKXgbOTBjB+MMEoTs7ybFG2sNml5mDUzNkhV3TvUHGu0xs2a53CubBu6QfDgjpR2weLPLVxmC9hwWl/xvssiqyEfA5Bz6hXChG9o1JxzdySoKTKamzyCiOuyvQADWibrqG2Vf+4q6p3aQmjDCBhrpq1/kGUghYFGEWGWAq8Jd80UD6SahZlmdLJuAq2vKjXiyc1zSBWkUpzBpMlY3L5eq/jFVG99IAdqTxB62RuWrlwg1adqOd+uZZsM2UaZkyZkL3X8wU3C2ObnvGsDBq+fKaYlsl0FTQttkVi9kmJCHFfzxuUXXLd622Fq82S9HCyd2FK/c1k6oAR4Pe7nPROwDTHWUAvvE5sD3wUxDUdFB06rK8Q6/yBMERfDEOt2V7H8y7sBWmG+swk688mSZlqJYDcMoVbZLk1sKKpyT7Pz2h1XqLjKy8h0A+tB3lYkBVzN9/SvqUxpsCDLXZWtSwKT2FtlgZZh8B92vTJ2ao99P7Wpgosu9KqwlJwNh6AcQ0qEzQLBwIdHpadxa052T7EgIZSC8F/b8JYqxhPb+JqBjx2Z6mXUfvJcL1SNuMLO17cMRtr+j8uv2AGDnk5y6pXFpG1aWJhY8q0sim422bKyCIeFzviCiz9M4rqnHCnDi3+uWnzeFYMLrKgn26zk5TSNemCloIfUaa/xsJy6Zw1b+lKbVYKwSXtLGqfeNEmZxBQ7RKY6L27DLxjS5yo2XkeUldNomu5HWwyCY6CNhPYsI1SuPzN9ZrHzOZegvQaXgZboVUvPtUUrkBJxlfUgszmodWDVVdhcPbiFEgumCC22ABYGC7QElE3L1EPZ6JPn/P7dV/U7ljlYQpxMhim18ODZMDJSXEdFJBlAMiHA4hBd1Ha3KQw2wiqVb9ZPCryqDbzQXL84LrgjomTIijSRV3MqBRIl1wRYwKdVRj7OKUt9hPftvipTDA3FMXpMMfSzKmxoSdmKnSKuzoTyr0lCK/AOLLSKKUA3V59pUhbp/QKvc3t737h8pam3ahhRLGvRRK04aTnAK6EknrdWL63JDNLzvL780uQYoTsIY4r1CMlfnCrKzMedeZaF4qmR+2QapFBT/oKrEcy41vNi05t3v/C83t58H3LqFQg1OugNaEl7Jq4Cy9Chr2C+bSwjtFwdgaU4inuY5bYwDnmZkizTO5dfelHvgA9RaKvhOsnneoC0FAJrcKD5C1wrWFpmC42KOvA06QBxHaQsojBUEW4c04mVhjJG1QZcCzDBR1IKtIooQuGp0LK3UCGlX8eGkCWNN9rgRPr17N7+dyy+WG7FlBdNuAaKanDGW4Kq5Auq/rdb0IKm2UJNWFlaKZkpkoXWYn+hJBoxpIo5n5oF6P/qntcvQx+dUw+iq0muGQVDaJ1ZzN1zRkrjOJUM8yCx15vaQKMTP7wSSMkx8OnkJp9i5p7dCy6UbBoAgmdkgVS91oIqn+5yM6BlWWwGvEp5uQq86G9Lg8W+EoRuPJp1tXWacNMbr00VWknTlJ1lIS+G7r9+yVfOFkM2F7LRcG5vz/t3XQNsT0GqxixFzRjhZV0ZrTmCTOL0cjDQK2sgq3Bm45lP+IOV+JJBmU0IdmNonKsmneCpsVKvDqQqCsMqR8rXzzU/79v1mqwEyxL11K7KT8mo6PVDlt+EdVVVL50xdMOEe8iakZOy6f9FsuKT+IZ9UYsk6nsvX3rOaxcvs8Zs3ug2dlKZAi8tY28gTKsT48e0pIEMwtSajAxF2wl+GhGcIW3CW3aYvsMwD0DLzkAxNwEp8NHn6KctJUuvW7r8FYvkCfbEYDd/UCKDYEuapjqDW/XobwGedJagBy0YA6WzQB7+2St+Nfb6g/64HeanxarNb6AbkeyT/7zrVfePn7xz9JAAIbQYQht4ApoCb36klRZLuDh0mrc+t1oDFCazTZnLNNuAj32JYvf4OB9KZWsD1jrcuzd4NZqkWklUwN4I3Ng+H/OSwXnv2flK86BLWp9E8YlfYJu9VwKvHChzKmRrxVXa4laRldDqNHTIsoruuYVBWR27EXvZMu0t9gYf2femC5p9rLKiS8haK89tbV1Pumlfxt2KlyiDgNU6D6l93gsEpZl1qNGBGT9hRHQbAz5giX/SutfG6LQYTqYNM4IVmK3sTrVjmA/Y4oX9Z3x475toKbKsSk0U7bRotHS9aLNLNAtOi7jaImBVllZgEM1sjQqRufjMnRI90/QGvfIb+lQi9pv730ZdCZzqBve9xK4PA+dFQ5l+5GYgNnbAzHw1xTAmK3kNPGzoFNbTT7x+X3h59wX8o1vXemaes4XeVuaU9EbRL3ugt/sje9+0q9lBS+HL0vRlrSaVYFnexnNktlRcbbXE8glgRj1oSUVJm8mynQwF8hAlSatYpkzV5BYPvZ0f2fuW3YMdIthZqoiBFQaOtGEZFXzW6F0QFpK2fJyvBmoyMhgcGxcVxlfqZLAwZy7099D+bs6QQZii14FU0gSOVhiuXf3F39jzpgO9XT0l+VgbclYILRctGutESZvRHCy3PdJWd7ndUrphCvVQK0T+Tfcf35FF4MMge4v9QeGXL1g88OE9b9zdX7ZMOG3VqVLIGiK0LsC6CBMp0hoGQwFreO4J6DN+sJZ08qbkePHG3hN4YhHVVnOj0dxS9H5v9GU/sufNFyw9g75+keK0FLQy2cth0cVhQV/AjhKctvhbccW3bBIGxryn5N2IdOxq4SlTS2OjafrcOLWr6+vD8XCENH+9HaV21I6GOB61Y9rzwPoTHz30xcfbY3FOn9dQSKFNZ+xj8koCCNkJdZS7Sx7iSd8unaxfjLNVkqVpVAyCZFn4THUvetQEDOrD8xv733phfz+LqEGJd/XFuiLpvrA4GHDUuZetdWvmA64Ea3F1BgJrKrYwNMVrkc0MJGzl4dhI2FodthlM5CESsAhShq1D45WPPvHFe8ePpxSGvOs0PwxJqB0XD0JRXcwBrmAVCq5ODKw4V6re71X92O3fFsalYGhFGqewpvN7+39z/1uoWtBRVYBF6Vb0dIFY5cGihJ+bkIsbEpFPF6q2Glgp1R3wVGiJZio9gUhgtXm0fRZUtOf4+lqGVB4PKtgaITESI/rr0fb4fz30lZ8MH4wNfxL6TRoEWEBYkE91kVpsEvsUJFbFBE9KpoRdPPlLrXN9aPPwvP4zP7zvzXuaHT32YzQ8X1CVA887FhatJJOzRtlgtQiAtzYB2GJUnT5gTVOIJUXUsEUSK8Ori60ssUZFaJF+HNNIun88etMXj/8Q635S3udsCsKClqyVIMwIz244PqB63i3FwVrfTcVTSh1I0X/XLF783t1XZ2YhSQSCUxgMVUuDhRIozCU4DbfwKahKaq2eRiV4eoB1MsZW1oYlJjwupNJwNFwfjxhbGWpZdLlOpJ/vrN71Pw9fv4JDgI46mkBYRY9W7FpXuz0F36SynKo1jrJ6Op7EMEiU5PiBPa+lYIO5fgN2AEuxCaNqsT8ooZucMsrJfbBtTKvTCayuQkxW+CsSq4OtrBbb8fpoSPYWq0ia9TsyYNHO1D46PvLfnvjyT4eP+mT60O2nQlhQDWE8b8dzQjihNoRKB3anB+u/mCa0LFRTlkKtOF7QP/ChvdceXNjHHh8Dq9ChwsX0S9lcI5x7B1UcyUhuWp0mJXg6gTULW97dehq21khyFaOe8ER2GEPK5Bapxc8euenLx28d4ijFju9RhtkMwwiJBBuqPZi1chsqSKza8kmtTSWfgpOARHu+afnKd+562UJReRK0KfDKfJ7m8S2WpMipqOoa7KcVVdsAWKkqWvBSqmnYIl1JcottLIYUm/lscvGeR0dHPnP0Gz9Yvc8mHE0ibBrI6ukmp6BAsFaCQQtOgmkCT/yCKxYOvm/P1Qf7e3sMpoIhNskNXqQBOca8EaqqUrvTpgRPM7A2MLZmYKstKXRt1onjEYuuvJPwRBtFbrEwI7biO2t3/92xG58YHo0ztDCa5zhNrVUsA5zC9wi8wxTlGbr2RzzRP3uapXfvftVVS5fkDGNNWOCUoSZUAJRQ4KApKe09LZqYgqptYFptC2CdPLY4K7NF5k5Jdo2Pr68y2gq2kPE0FmzlDbLlv3Lsh6QZD41WUt33JgxSmYYzmJBtG5/5xFsq86sGk+nIvb0dVOX2xp1XlhqbnqaA9hhhVsFL58i5ez1tedWohb6dUXX6gXWy2ALht5hEpd/ZMVTR1eZBEGx7ZeSx6Cpasl0drd+w+pPPr/zgsdGRcN0rhVS7+lNPEbuc1bS4G0azLWEtw/xb7h/sunbpimt3PH9Hf7GnFW+SrCfpxU3xAbMGpLIITn70HjJNA1bGtl1RtS2ANdWQ72LLGFQRXWJyUcxnOGK1yPYWA6vlDcbcuBCqX1+57evHb/vp6HGcoqmqCTP1NYGpEm2K2Kpzyad+ynP6z7hmx6XXLF82KCmyjJ7GMv21QJ7DyZRfRWURjdWgajsxaw8wBVWn22DfrsCahq0kfTEEVUrQF8OL4FXU4mg8ItHFwBqzRZ/aAj4JO+ad+cXto6Oj31y9/fqV2yjOeIITOol8AHQdt9HL9vWWX7p00Rt2PD+b5xrXkw1uY5EklaPRnA5O1O55m1qP1RgLmqzPM3YTk84C69SwZU07zOQyoz6TEaP1lrcTm/ki1djAHxc2X/a34x8PH/zu8btuHz58/+jQZox3pwt+fm/vZQsHX7b0nMsXDlrjk0aagWlnlNR0NhZK8VYT8FQZVdJcc7ujahsBazq2NjC5RICJ6GK4DMeZ7hI8MYzQLTMWZuoEyM5j49Vbhw/cuv7g7WsPEcs6Tu1TPn8CxzN7uy9dPO/yxYNXDi5YbhYNQNFI6rkNbpBifddQGRzDzgSVqL8ZRlVK2xRV2wtYExTjVJNLqFTj6FV0JUPPKMd/xoKesqeU/YlT6WiTcTitxb/J6ica7CE8fP/644+0Rx8dHlnBteO4vtoOCb7rieaTpYWUDaClZrAMiztg4ZzB7mc2uy4YHDiv2XNOf3eZjpS0XXEj9W0GLw7qlSZFKrfEeGfdZ+kJNsxRjiOD1tMso2q7oWrbAeuE2Erai3pSdLWR9GrbCC/bz9uGxXFJ9BvrEUwoJpuF6f1hOtSpjRj2ZDoTJ9quuMKWBfJ6tpOjNL38E/dPCirpXjFb/W03VG1HYG2kFmt2nltMtQUcrSIjmF+pSK/xmOGlCtFRCBaaZKEV2pfGDrmVla4upGdc+cSHZK3mGRgMLE+OFSGkmo4h1Tf51ARcJhdU2GHVt7P6mwNgTWJrI9FVmV8y702lFJlaTEyMTHpF9ef9GqSXg3ec1UHLYYJ8aFOatIeldogBm+QWOhdBUGoiujLb2XgLWq8sRRVL3phmhqDa9qja1sDaSC2mKVaXzZ7AIL1EIBXhlKn5NhP0kv6lzIXDK3nHbIyU5+QSgRNWLqvAS5CTTxyRpAMGU04gFh3XxAYWUwLJ0aJK86H+5gZYJ+MtJi/gA0NVUI6T6MkWWNsKg5pqVCWd8x3nJs3kVEOzXCvsjpqRuc2eDvqu1Fzy2aepglS02+bA+5tjYD0VeGlTodaG2ah+DIxrfmSfkfVjCRnZKOvQ333yUsZkhjBnubxIMFR6y0p/iiDVXFeeuZCaM2CdjGZMNuBElaM3nJmCMLCE1SQFMq38Fat0z05DZK0gDHPzbFINNKGfL4TYS6rwxB2LgnEWes7Oq+6bY2CdwKifhFecQzghzGz0c4jL+J/qkfc41cSKQ8d0p7TfNZqgmkkQvTwd2bURpOZQUM0xsKaKro3hFXpfoxbyoFlR6DPOQx/vwG7MCtp0cAa2KTOpFHNBPiUtfu8OpJwNqXlE1bwC6wTwSrENdxdhKbh+iiqIOJsJptl1Fl1bXqcVV2BKaYqICqd6xkBq7oF1UvBKVa91jIMmK44KO69MEFu8zVq86jUQATYBpmDgQwemZxik+PH/AQYYSBDpTYUEAAAAAElFTkSuQmCC", - "description": "Visualize the state of the device. Fetches the value from device using RPC or using attribute subscription.", - "descriptor": { - "type": "rpc", - "sizeX": 2.5, - "sizeY": 2.5, - "resources": [], - "templateHtml": "", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-led-indicator-widget-settings", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":true,\"title\":\"Led indicator\",\"ledColor\":\"#4caf50\",\"valueAttribute\":\"value\",\"retrieveValueMethod\":\"attribute\",\"parseValueFunction\":\"return data ? true : false;\",\"performCheckStatus\":true,\"checkStatusMethod\":\"checkStatus\"},\"title\":\"Led indicator\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" - } - }, - { - "alias": "rpcbutton", - "name": "RPC Button", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAN8SURBVHja7dvbSxRRHAfw/X9+bhcvrJZSmllhZFSCWBDaY4FUhBRZCQXR7akLyoJYlqxU0g0tukF0oYQM3bxsYVRgbuuyu7k7szNn9vSb2Wx9qB56ac72/T7MHGYZmA/nMjvDbzzSTkbpOASPw7CUjkPx2AwhTNNQNKYphE1hiCXMtK5pWkrB8GXraZMl0sMOQ9dEdqApmIzQdIMlDDH1lLIMh5LSTYZYIp1MS6XDAGF5uEPiltoQK85dwhAtKhVPVLMhRj5ADAcyqzpkNgtJqQ9JAQIIIIAAAggggAACCCB/8wog8btfAjd+f9Y3t0GmW5bS8rO/fvavaXB2jWVlZbVHwjLC+7L1x2P8rN1ZQcWtX90EyWws7r7bSuf+BKnz+f1t3norTPV+/0HvNiFP0+5b54sahYsgn+mQrdksZfL6hYcZGe2betoxyG/LtP7OiXlIJW/a6F2Y9nPjKI3MLtrBjbM05CJIsqT6tdOI1BbWFuyRQVpbupw7yGygqqLCBZCTNJqFdNGTAQpwY2bwo5vmyL1iqr+uS3l4yaTsoaEg7bT0VXWyn/xyquQHpDz4JuCrFWHaG48OrSuJXqLHLly1Yl01VDkuq6r7+i5Qd5ABsqlctlMiN0eI0/BZhu09rXggu10J4TVoYMlWWeKr51x2IM3lcp83k4NUfAqVbrQYsn3w/lvuvTt0lQ9Hn027CNJLt3m7YaXctIaXYCHnIWfog5Src3Okg/pldo7Ys8O7S9qHXrgIEllWee35uYJ9MkAHXgVq3s9Dxguan56iHCRZUa3/hPDAa3/U7dtiuGloTTZ7aXFrnO9xPio8Zs5D5JViaqpbsGpdpK4cxDxRRItbpl02R1Jfsje2zIy+8LCI/ekkc0a4b7Lj3y8ggAACCCCAAAIIIIAA8v9C8qaoRosqXngmsmVOXHgWVxsSyxaemXosJFR2mBMxpxRQpOPB0YihKsOIjAbjdnEml8vOhUaGX97s7VEwvTdfDo+E5kyn7pe7JDQWVDZjIbtDMk5JuZGcSyQScQXDlz2XNJyS8vwp8s+bzy6kzJsPYfIigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg/yTfATinNXQTM7TQAAAAAElFTkSuQmCC", - "description": "Allows to send RPC command when user press the button.", - "descriptor": { - "type": "rpc", - "sizeX": 4, - "sizeY": 2, - "resources": [], - "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n
\n
\n
\n {{ error }}\n
\n
", - "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", - "controllerScript": "var requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n \n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n if (rpcParams.length) {\n try {\n rpcParams = JSON.parse(rpcParams);\n } catch (e) {}\n }\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n }\n commandPromise.subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n );\n };\n}\n\nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-send-rpc-widget-settings", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":5000,\"oneWayElseTwoWay\":true,\"buttonText\":\"Send RPC\",\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"methodName\":\"rpcCommand\",\"methodParams\":\"{}\"},\"title\":\"RPC Button\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_attributes", - "name": "Update device attribute", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAWhSURBVHja7dv7b1NVHADw/T/fW/dk3SM6B3NTBkoGQ+kkBFeRiVHDL4jxARghZERmcEzqYBMqk+sCyTaUoQQhcwJFBUaZUBDHxmNjK+vWtbf30a/fc2/LY8JCNoy5zff7w93t6bnfnk/POe3N2WkaiojbOkxCmskwbB0mJU0wdF3TVJuGpum6oBDE0LWYEo1GIzYMarYS00iCaeRQlahuDTQbRlyPKipJCKIpEdsyTEpE0Qhi6LGJGNo6CKAbadQhIcPeECNEXUKQaBBtHsGogKipAFFNyIjdISMWJGJ/SIQhDGEIQxjCEIYwhCEMYchUcaHx5lRPDzRenFSiX3n85OM3cKjx3BOHPOemw7n8nQ8UtoDv7vqFf+Bf13RD66SSb+HUFI247qcVg96+xKN3s9UeaHqgguq/NWOI00WHP6D+URAV3nsMyMC28Ska8RGEEXNXJq/24mTITah9cpDDx3o9+yP04Kyn3SsgvoamSzjUAq4DiKF9O35JXHBlp/yTgAx7PadRkakn+uWrl+Uheuabr3qogtK2vTOxZnO75Qs67VwK3h45a4FsHO8IeILdrQTZdchzErFPvo7YcaS/Edw/JDLOHFKRlzMbKlVsd+QWOgmyGSpLHEfPlcKsCuwrzpkLG836vuysIidB/AXOMvgS589H3CINiC488lRhieTF8UXp8xzVpsTvfNol1cTfyIXSpjJHdplek+WEwDvpBMnLy4dm7IAjiHOqjpeAsxr9hWbGJwC5gfXQpj37/Fh8DfgU9zaM5q2whtZbzutY5/hL1HflDlC1VlxSHMQPM0ca4BrOd4mxqM2uiBg1OXqd5MODcFjUbXIHcRP03RtaNVLLkGpCVhnKYudEAmINLZeVceaQclpzgY19UGfNkXh382cFC02IkblAlrdAh1goS19tzpE7UCXL6+BEH+y+Bl5xwVXYQdO6W1lUIMu7RRKKW/u2VcPv90FyqdCE0Bxpggv3Q0ZFxvXw63Qh+a/Q4TdoMCHUar94DWqX8VbW2iQkAoWVFAIShvUmpB+KRFEXvlztdQyJC86SR0RptniiQZweTXfVPhrSCifuhwwkM04TUvmMhrgfDpqQi1AXhHWIe8EXIBuWWxAsElrdrF+wHLELWrWMVVbR1+lLqk35CGyirm2MuPOVZN1lxQbueTikkaYW/N0JnYizLYie8ebdF5kOxAOrj+0vzg9iheQ5uUzqxRVZ8o+l4OuXXvNtlV7CePaLXTQpNp1uLhsU9T+VPD8vpjmyQar3bZ83joMO8REmxuIqZ3vXC+XGYXjbd6D0rKi7Mqvz0Bz6hqmFvbdwTskx7R6k6Pv2vIXxS/B6dy1UUT+7fPiJVH9aZJwmxPDkg2NpL2JF0auQtYeG+WJwrqV27coA16JZMdwK6ahuzYFZn5v/mBhbKWV+TG2PbMiE/GYqWJ4RsiDDNQ6ovEznhZCxNirqBuaCcw18h38WgIy7JBi/B3m/AMovIW6WwF1ahfgBFFsZm2ZyizKsiCMNrTua+fiO9S0QS6wXj4aFd1BNVg9piduSQW1SImXY+juoJEtuW6nUIfEWPPBmG1b2yFhyafehGadzryXmSErcNNa4+TaeIQxhCEMYwhCGMIQhDGHIfwBJmU010aDNN57p1jYn2ngWsjdk1Np4pimjAd3ODu3iqLkVUI+F/OeHVbsy1OHz/pDYnEnbZcOBnjOn2lq8NoyWtlNnegJhzdz3S10S6PXbNnoDokPi5pZydSI8NjYWsmFQs8MTqrmlPHU2+afMzy4QU+aHMCkRDGEIQxjCEIYwhCEMYQhDGMIQhjCEIQxhCEMYwhCGMIQhDGEIQxjCEIYwhCEMYQhDGMIQhjCEIQxhCEMYwhCGMIQhDGEIQxjCEIYwhCEMYQhDGMIQhjCEIQxhCEMYwhCGMIQh/0v8A7Y7NXI35bvdAAAAAElFTkSuQmCC", - "description": "Allows to send shared attribute update when user press the button.", - "descriptor": { - "type": "rpc", - "sizeX": 4, - "sizeY": 2, - "resources": [], - "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n
\n
\n
\n {{ error }}\n
\n
", - "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n let attributeService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n let entityId = {\n entityType: \"DEVICE\",\n id: self.ctx.defaultSubscription.targetDeviceId\n };\n attributeService.saveEntityAttributes(entityId,\n entityAttributeType, attributes).subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n\n );\n };\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-device-attribute-widget-settings", - "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"entityParameters\":\"{}\",\"entityAttributeType\":\"SERVER_SCOPE\",\"buttonText\":\"Update device attribute\"},\"title\":\"Update device attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"targetDeviceAliases\":[]}" - } - }, - { - "alias": "persistent_table", - "name": "Persistent RPC table", - "image": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yMDAgMEgwVjE2MEgyMDBWMFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMDAgMTIwSDBWMTIxSDIwMFYxMjBaIiBmaWxsPSIjRTBFMEUwIi8+CjxwYXRoIGQ9Ik0yMDAgODBIMFY4MUgyMDBWODBaIiBmaWxsPSIjRTBFMEUwIi8+CjxwYXRoIGQ9Ik0yMDAgMzlIMFY0MEgyMDBWMzlaIiBmaWxsPSIjRTBFMEUwIi8+CjxwYXRoIGQ9Ik0xNS42Njg1IDE5Ljk4NjhIMTQuMTUzOFYyM0gxMi43OTQ5VjE1LjE3OTdIMTUuNTQ0OUMxNi40NDczIDE1LjE3OTcgMTcuMTQzNyAxNS4zODIgMTcuNjM0MyAxNS43ODY2QzE4LjEyNDggMTYuMTkxMiAxOC4zNzAxIDE2Ljc3NjcgMTguMzcwMSAxNy41NDNDMTguMzcwMSAxOC4wNjU4IDE4LjI0MyAxOC41MDQ0IDE3Ljk4ODggMTguODU4OUMxNy43MzgxIDE5LjIwOTggMTcuMzg3MiAxOS40ODAxIDE2LjkzNiAxOS42Njk5TDE4LjY5MjQgMjIuOTMwMlYyM0gxNy4yMzY4TDE1LjY2ODUgMTkuOTg2OFpNMTQuMTUzOCAxOC44OTY1SDE1LjU1MDNDMTYuMDA4NiAxOC44OTY1IDE2LjM2NjcgMTguNzgxOSAxNi42MjQ1IDE4LjU1MjdDMTYuODgyMyAxOC4zMiAxNy4wMTEyIDE4LjAwMzEgMTcuMDExMiAxNy42MDIxQzE3LjAxMTIgMTcuMTgzMSAxNi44OTEzIDE2Ljg1OSAxNi42NTE0IDE2LjYyOTlDMTYuNDE1IDE2LjQwMDcgMTYuMDYwNSAxNi4yODI2IDE1LjU4NzkgMTYuMjc1NEgxNC4xNTM4VjE4Ljg5NjVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0yMS4wMTgxIDIwLjA5NDJWMjNIMTkuNjU5MlYxNS4xNzk3SDIyLjY1MDlDMjMuNTI0NiAxNS4xNzk3IDI0LjIxNzQgMTUuNDA3MSAyNC43Mjk1IDE1Ljg2MThDMjUuMjQ1MSAxNi4zMTY2IDI1LjUwMjkgMTYuOTE4MSAyNS41MDI5IDE3LjY2NjVDMjUuNTAyOSAxOC40MzI4IDI1LjI1MDUgMTkuMDI5IDI0Ljc0NTYgMTkuNDU1MUMyNC4yNDQzIDE5Ljg4MTIgMjMuNTQwNyAyMC4wOTQyIDIyLjYzNDggMjAuMDk0MkgyMS4wMTgxWk0yMS4wMTgxIDE5LjAwMzlIMjIuNjUwOUMyMy4xMzQzIDE5LjAwMzkgMjMuNTAzMSAxOC44OTExIDIzLjc1NzMgMTguNjY1NUMyNC4wMTE2IDE4LjQzNjQgMjQuMTM4NyAxOC4xMDY5IDI0LjEzODcgMTcuNjc3MkMyNC4xMzg3IDE3LjI1NDcgMjQuMDA5OCAxNi45MTgxIDIzLjc1MiAxNi42Njc1QzIzLjQ5NDEgMTYuNDEzMiAyMy4xMzk2IDE2LjI4MjYgMjIuNjg4NSAxNi4yNzU0SDIxLjAxODFWMTkuMDAzOVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTMyLjY2MjYgMjAuNDU0MUMzMi41ODM4IDIxLjI4ODQgMzIuMjc1OSAyMS45NDAxIDMxLjczODggMjIuNDA5MkMzMS4yMDE3IDIyLjg3NDcgMzAuNDg3MyAyMy4xMDc0IDI5LjU5NTcgMjMuMTA3NEMyOC45NzI3IDIzLjEwNzQgMjguNDIzIDIyLjk2MDYgMjcuOTQ2OCAyMi42NjdDMjcuNDc0MSAyMi4zNjk4IDI3LjEwODkgMjEuOTQ5MSAyNi44NTExIDIxLjQwNDhDMjYuNTkzMyAyMC44NjA1IDI2LjQ1OSAyMC4yMjg1IDI2LjQ0ODIgMTkuNTA4OFYxOC43NzgzQzI2LjQ0ODIgMTguMDQwNyAyNi41Nzg5IDE3LjM5MDggMjYuODQwMyAxNi44Mjg2QzI3LjEwMTcgMTYuMjY2NCAyNy40NzU5IDE1LjgzMzIgMjcuOTYyOSAxNS41Mjg4QzI4LjQ1MzUgMTUuMjI0NCAyOS4wMTkyIDE1LjA3MjMgMjkuNjYwMiAxNS4wNzIzQzMwLjUyMzEgMTUuMDcyMyAzMS4yMTc4IDE1LjMwNjggMzEuNzQ0MSAxNS43NzU5QzMyLjI3MDUgMTYuMjQ1IDMyLjU3NjcgMTYuOTA3NCAzMi42NjI2IDE3Ljc2MzJIMzEuMzA5MUMzMS4yNDQ2IDE3LjIwMSAzMS4wNzk5IDE2Ljc5NjQgMzAuODE0OSAxNi41NDkzQzMwLjU1MzUgMTYuMjk4NyAzMC4xNjg2IDE2LjE3MzMgMjkuNjYwMiAxNi4xNzMzQzI5LjA2OTMgMTYuMTczMyAyOC42MTQ2IDE2LjM5IDI4LjI5NTkgMTYuODIzMkMyNy45ODA4IDE3LjI1MjkgMjcuODE5NyAxNy44ODQ5IDI3LjgxMjUgMTguNzE5MlYxOS40MTIxQzI3LjgxMjUgMjAuMjU3MiAyNy45NjI5IDIwLjkwMTcgMjguMjYzNyAyMS4zNDU3QzI4LjU2OCAyMS43ODk3IDI5LjAxMiAyMi4wMTE3IDI5LjU5NTcgMjIuMDExN0MzMC4xMjkyIDIyLjAxMTcgMzAuNTMwMyAyMS44OTE4IDMwLjc5ODggMjEuNjUxOUMzMS4wNjc0IDIxLjQxMTkgMzEuMjM3NSAyMS4wMTI3IDMxLjMwOTEgMjAuNDU0MUgzMi42NjI2WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMzguMDU1MiAyM0gzNi43MDE3VjE1LjE3OTdIMzguMDU1MlYyM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTM5LjcyNTYgMjNWMTUuMTc5N0g0Mi4wMzUyQzQyLjcyNjIgMTUuMTc5NyA0My4zMzg1IDE1LjMzMzcgNDMuODcyMSAxNS42NDE2QzQ0LjQwOTIgMTUuOTQ5NSA0NC44MjQ1IDE2LjM4NjQgNDUuMTE4MiAxNi45NTIxQzQ1LjQxMTggMTcuNTE3OSA0NS41NTg2IDE4LjE2NiA0NS41NTg2IDE4Ljg5NjVWMTkuMjg4NkM0NS41NTg2IDIwLjAyOTggNDUuNDEgMjAuNjgxNSA0NS4xMTI4IDIxLjI0MzdDNDQuODE5MiAyMS44MDU4IDQ0LjM5ODQgMjIuMjM5MSA0My44NTA2IDIyLjU0MzVDNDMuMzA2MyAyMi44NDc4IDQyLjY4MTUgMjMgNDEuOTc2MSAyM0gzOS43MjU2Wk00MS4wODQ1IDE2LjI3NTRWMjEuOTE1SDQxLjk3MDdDNDIuNjgzMyAyMS45MTUgNDMuMjI5MyAyMS42OTMgNDMuNjA4OSAyMS4yNDlDNDMuOTkyIDIwLjgwMTQgNDQuMTg3MiAyMC4xNjA1IDQ0LjE5NDMgMTkuMzI2MlYxOC44OTExQzQ0LjE5NDMgMTguMDQyNSA0NC4wMDk5IDE3LjM5NDQgNDMuNjQxMSAxNi45NDY4QzQzLjI3MjMgMTYuNDk5MiA0Mi43MzcgMTYuMjc1NCA0Mi4wMzUyIDE2LjI3NTRINDEuMDg0NVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTYxLjU1NjYgMTUuMTc5N0w2My44MTI1IDIxLjE3MzhMNjYuMDYzIDE1LjE3OTdINjcuODE5M1YyM0g2Ni40NjU4VjIwLjQyMTlMNjYuNjAwMSAxNi45NzM2TDY0LjI5MDUgMjNINjMuMzE4NEw2MS4wMTQyIDE2Ljk3OUw2MS4xNDg0IDIwLjQyMTlWMjNINTkuNzk0OVYxNS4xNzk3SDYxLjU1NjZaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik03MS44NjM4IDIzLjEwNzRDNzEuMDM2NiAyMy4xMDc0IDcwLjM2NTIgMjIuODQ3OCA2OS44NDk2IDIyLjMyODZDNjkuMzM3NiAyMS44MDU4IDY5LjA4MTUgMjEuMTExMiA2OS4wODE1IDIwLjI0NDZWMjAuMDgzNUM2OS4wODE1IDE5LjUwMzQgNjkuMTkyNSAxOC45ODYgNjkuNDE0NiAxOC41MzEyQzY5LjY0MDEgMTguMDcyOSA2OS45NTUyIDE3LjcxNjYgNzAuMzU5OSAxNy40NjI0QzcwLjc2NDUgMTcuMjA4MiA3MS4yMTU3IDE3LjA4MTEgNzEuNzEzNCAxNy4wODExQzcyLjUwNDcgMTcuMDgxMSA3My4xMTUyIDE3LjMzMzUgNzMuNTQ0OSAxNy44Mzg0QzczLjk3ODIgMTguMzQzMyA3NC4xOTQ4IDE5LjA1NzYgNzQuMTk0OCAxOS45ODE0VjIwLjUwNzhINzAuMzk3NUM3MC40MzY4IDIwLjk4NzYgNzAuNTk2MiAyMS4zNjcyIDcwLjg3NTUgMjEuNjQ2NUM3MS4xNTg0IDIxLjkyNTggNzEuNTEyOSAyMi4wNjU0IDcxLjkzOSAyMi4wNjU0QzcyLjUzNjkgMjIuMDY1NCA3My4wMjM5IDIxLjgyMzcgNzMuMzk5OSAyMS4zNDAzTDc0LjEwMzUgMjIuMDExN0M3My44NzA4IDIyLjM1OSA3My41NTkyIDIyLjYyOTQgNzMuMTY4OSAyMi44MjI4QzcyLjc4MjIgMjMuMDEyNSA3Mi4zNDcyIDIzLjEwNzQgNzEuODYzOCAyMy4xMDc0Wk03MS43MDggMTguMTI4NEM3MS4zNDk5IDE4LjEyODQgNzEuMDU5OSAxOC4yNTM3IDcwLjgzNzkgMTguNTA0NEM3MC42MTk1IDE4Ljc1NSA3MC40Nzk4IDE5LjEwNDIgNzAuNDE4OSAxOS41NTE4SDcyLjkwNThWMTkuNDU1MUM3Mi44NzcxIDE5LjAxODIgNzIuNzYwNyAxOC42ODg4IDcyLjU1NjYgMTguNDY2OEM3Mi4zNTI1IDE4LjI0MTIgNzIuMDY5NyAxOC4xMjg0IDcxLjcwOCAxOC4xMjg0WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNzguNDcwMiAyMS40MjA5Qzc4LjQ3MDIgMjEuMTg4MiA3OC4zNzM1IDIxLjAxMDkgNzguMTgwMiAyMC44ODkyQzc3Ljk5MDQgMjAuNzY3NCA3Ny42NzM1IDIwLjY2IDc3LjIyOTUgMjAuNTY2OUM3Ni43ODU1IDIwLjQ3MzggNzYuNDE0OSAyMC4zNTU2IDc2LjExNzcgMjAuMjEyNEM3NS40NjYgMTkuODk3MyA3NS4xNDAxIDE5LjQ0MDggNzUuMTQwMSAxOC44NDI4Qzc1LjE0MDEgMTguMzQxNSA3NS4zNTE0IDE3LjkyMjUgNzUuNzczOSAxNy41ODU5Qzc2LjE5NjUgMTcuMjQ5MyA3Ni43MzM2IDE3LjA4MTEgNzcuMzg1MyAxNy4wODExQzc4LjA3OTkgMTcuMDgxMSA3OC42NDAzIDE3LjI1MjkgNzkuMDY2NCAxNy41OTY3Qzc5LjQ5NjEgMTcuOTQwNCA3OS43MTA5IDE4LjM4NjIgNzkuNzEwOSAxOC45MzQxSDc4LjQwNThDNzguNDA1OCAxOC42ODM0IDc4LjMxMjcgMTguNDc1NyA3OC4xMjY1IDE4LjMxMUM3Ny45NDAzIDE4LjE0MjcgNzcuNjkzMiAxOC4wNTg2IDc3LjM4NTMgMTguMDU4NkM3Ny4wOTg4IDE4LjA1ODYgNzYuODY0MyAxOC4xMjQ4IDc2LjY4MTYgMTguMjU3M0M3Ni41MDI2IDE4LjM4OTggNzYuNDEzMSAxOC41NjcxIDc2LjQxMzEgMTguNzg5MUM3Ni40MTMxIDE4Ljk4OTYgNzYuNDk3MiAxOS4xNDUzIDc2LjY2NTUgMTkuMjU2M0M3Ni44MzM4IDE5LjM2NzQgNzcuMTc0IDE5LjQ4MDEgNzcuNjg2IDE5LjU5NDdDNzguMTk4MSAxOS43MDU3IDc4LjU5OTEgMTkuODQgNzguODg5MiAxOS45OTc2Qzc5LjE4MjggMjAuMTUxNSA3OS4zOTk0IDIwLjMzNzcgNzkuNTM5MSAyMC41NTYyQzc5LjY4MjMgMjAuNzc0NiA3OS43NTM5IDIxLjAzOTYgNzkuNzUzOSAyMS4zNTExQzc5Ljc1MzkgMjEuODczOSA3OS41MzczIDIyLjI5ODIgNzkuMTA0IDIyLjYyNEM3OC42NzA3IDIyLjk0NjMgNzguMTAzMiAyMy4xMDc0IDc3LjQwMTQgMjMuMTA3NEM3Ni45MjUxIDIzLjEwNzQgNzYuNTAwOCAyMy4wMjE1IDc2LjEyODQgMjIuODQ5NkM3NS43NTYgMjIuNjc3NyA3NS40NjYgMjIuNDQxNCA3NS4yNTgzIDIyLjE0MDZDNzUuMDUwNiAyMS44Mzk4IDc0Ljk0NjggMjEuNTE1OCA3NC45NDY4IDIxLjE2ODVINzYuMjE0NEM3Ni4yMzIzIDIxLjQ3NjQgNzYuMzQ4NiAyMS43MTQ1IDc2LjU2MzUgMjEuODgyOEM3Ni43NzgzIDIyLjA0NzUgNzcuMDYzIDIyLjEyOTkgNzcuNDE3NSAyMi4xMjk5Qzc3Ljc2MTIgMjIuMTI5OSA3OC4wMjI2IDIyLjA2NTQgNzguMjAxNyAyMS45MzY1Qzc4LjM4MDcgMjEuODA0IDc4LjQ3MDIgMjEuNjMyMiA3OC40NzAyIDIxLjQyMDlaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik04NC4xNTI4IDIxLjQyMDlDODQuMTUyOCAyMS4xODgyIDg0LjA1NjIgMjEuMDEwOSA4My44NjI4IDIwLjg4OTJDODMuNjczIDIwLjc2NzQgODMuMzU2MSAyMC42NiA4Mi45MTIxIDIwLjU2NjlDODIuNDY4MSAyMC40NzM4IDgyLjA5NzUgMjAuMzU1NiA4MS44MDAzIDIwLjIxMjRDODEuMTQ4NiAxOS44OTczIDgwLjgyMjggMTkuNDQwOCA4MC44MjI4IDE4Ljg0MjhDODAuODIyOCAxOC4zNDE1IDgxLjAzNCAxNy45MjI1IDgxLjQ1NjUgMTcuNTg1OUM4MS44NzkxIDE3LjI0OTMgODIuNDE2MiAxNy4wODExIDgzLjA2NzkgMTcuMDgxMUM4My43NjI1IDE3LjA4MTEgODQuMzIyOSAxNy4yNTI5IDg0Ljc0OSAxNy41OTY3Qzg1LjE3ODcgMTcuOTQwNCA4NS4zOTM2IDE4LjM4NjIgODUuMzkzNiAxOC45MzQxSDg0LjA4ODRDODQuMDg4NCAxOC42ODM0IDgzLjk5NTMgMTguNDc1NyA4My44MDkxIDE4LjMxMUM4My42MjI5IDE4LjE0MjcgODMuMzc1OCAxOC4wNTg2IDgzLjA2NzkgMTguMDU4NkM4Mi43ODE0IDE4LjA1ODYgODIuNTQ2OSAxOC4xMjQ4IDgyLjM2NDMgMTguMjU3M0M4Mi4xODUyIDE4LjM4OTggODIuMDk1NyAxOC41NjcxIDgyLjA5NTcgMTguNzg5MUM4Mi4wOTU3IDE4Ljk4OTYgODIuMTc5OSAxOS4xNDUzIDgyLjM0ODEgMTkuMjU2M0M4Mi41MTY0IDE5LjM2NzQgODIuODU2NiAxOS40ODAxIDgzLjM2ODcgMTkuNTk0N0M4My44ODA3IDE5LjcwNTcgODQuMjgxNyAxOS44NCA4NC41NzE4IDE5Ljk5NzZDODQuODY1NCAyMC4xNTE1IDg1LjA4MiAyMC4zMzc3IDg1LjIyMTcgMjAuNTU2MkM4NS4zNjQ5IDIwLjc3NDYgODUuNDM2NSAyMS4wMzk2IDg1LjQzNjUgMjEuMzUxMUM4NS40MzY1IDIxLjg3MzkgODUuMjE5OSAyMi4yOTgyIDg0Ljc4NjYgMjIuNjI0Qzg0LjM1MzQgMjIuOTQ2MyA4My43ODU4IDIzLjEwNzQgODMuMDg0IDIzLjEwNzRDODIuNjA3NyAyMy4xMDc0IDgyLjE4MzQgMjMuMDIxNSA4MS44MTEgMjIuODQ5NkM4MS40Mzg2IDIyLjY3NzcgODEuMTQ4NiAyMi40NDE0IDgwLjk0MDkgMjIuMTQwNkM4MC43MzMyIDIxLjgzOTggODAuNjI5NCAyMS41MTU4IDgwLjYyOTQgMjEuMTY4NUg4MS44OTdDODEuOTE0OSAyMS40NzY0IDgyLjAzMTIgMjEuNzE0NSA4Mi4yNDYxIDIxLjg4MjhDODIuNDYwOSAyMi4wNDc1IDgyLjc0NTYgMjIuMTI5OSA4My4xMDAxIDIyLjEyOTlDODMuNDQzOCAyMi4xMjk5IDgzLjcwNTIgMjIuMDY1NCA4My44ODQzIDIxLjkzNjVDODQuMDYzMyAyMS44MDQgODQuMTUyOCAyMS42MzIyIDg0LjE1MjggMjEuNDIwOVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTkwLjA1MDMgMjNDODkuOTkzIDIyLjg4OSA4OS45NDI5IDIyLjcwODIgODkuODk5OSAyMi40NTc1Qzg5LjQ4NDUgMjIuODkwOCA4OC45NzYxIDIzLjEwNzQgODguMzc0NSAyMy4xMDc0Qzg3Ljc5MDkgMjMuMTA3NCA4Ny4zMTQ2IDIyLjk0MDkgODYuOTQ1OCAyMi42MDc5Qzg2LjU3NyAyMi4yNzQ5IDg2LjM5MjYgMjEuODYzMSA4Ni4zOTI2IDIxLjM3MjZDODYuMzkyNiAyMC43NTMxIDg2LjYyMTcgMjAuMjc4NiA4Ny4wODAxIDE5Ljk0OTJDODcuNTQyIDE5LjYxNjIgODguMjAwOCAxOS40NDk3IDg5LjA1NjYgMTkuNDQ5N0g4OS44NTY5VjE5LjA2ODRDODkuODU2OSAxOC43Njc2IDg5Ljc3MjggMTguNTI3NyA4OS42MDQ1IDE4LjM0ODZDODkuNDM2MiAxOC4xNjYgODkuMTgwMiAxOC4wNzQ3IDg4LjgzNjQgMTguMDc0N0M4OC41MzkyIDE4LjA3NDcgODguMjk1NyAxOC4xNDk5IDg4LjEwNiAxOC4zMDAzQzg3LjkxNjIgMTguNDQ3MSA4Ny44MjEzIDE4LjYzNTEgODcuODIxMyAxOC44NjQzSDg2LjUxNjFDODYuNTE2MSAxOC41NDU2IDg2LjYyMTcgMTguMjQ4NCA4Ni44MzMgMTcuOTcyN0M4Ny4wNDQzIDE3LjY5MzQgODcuMzMwNyAxNy40NzQ5IDg3LjY5MjQgMTcuMzE3NEM4OC4wNTc2IDE3LjE1OTggODguNDY0IDE3LjA4MTEgODguOTExNiAxNy4wODExQzg5LjU5MiAxNy4wODExIDkwLjEzNDQgMTcuMjUyOSA5MC41MzkxIDE3LjU5NjdDOTAuOTQzNyAxNy45MzY4IDkxLjE1MTQgMTguNDE2NyA5MS4xNjIxIDE5LjAzNjFWMjEuNjU3MkM5MS4xNjIxIDIyLjE4IDkxLjIzNTUgMjIuNTk3MiA5MS4zODIzIDIyLjkwODdWMjNIOTAuMDUwM1pNODguNjE2MiAyMi4wNjAxQzg4Ljg3NCAyMi4wNjAxIDg5LjExNTcgMjEuOTk3NCA4OS4zNDEzIDIxLjg3MjFDODkuNTcwNSAyMS43NDY3IDg5Ljc0MjQgMjEuNTc4NSA4OS44NTY5IDIxLjM2NzJWMjAuMjcxNUg4OS4xNTMzQzg4LjY2OTkgMjAuMjcxNSA4OC4zMDY1IDIwLjM1NTYgODguMDYzIDIwLjUyMzlDODcuODE5NSAyMC42OTIyIDg3LjY5NzggMjAuOTMwMyA4Ny42OTc4IDIxLjIzODNDODcuNjk3OCAyMS40ODg5IDg3Ljc4MDEgMjEuNjg5NSA4Ny45NDQ4IDIxLjgzOThDODguMTEzMSAyMS45ODY3IDg4LjMzNjkgMjIuMDYwMSA4OC42MTYyIDIyLjA2MDFaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik05Mi4zMDA4IDIwLjA1MTNDOTIuMzAwOCAxOS4xNDg5IDkyLjUxMiAxOC40MjkyIDkyLjkzNDYgMTcuODkyMUM5My4zNjA3IDE3LjM1MTQgOTMuOTI0NiAxNy4wODExIDk0LjYyNjUgMTcuMDgxMUM5NS4yODg5IDE3LjA4MTEgOTUuODA5OSAxNy4zMTIgOTYuMTg5NSAxNy43NzM5TDk2LjI0ODUgMTcuMTg4NUg5Ny40MjQ4VjIyLjgyMjhDOTcuNDI0OCAyMy41ODU0IDk3LjE4NjcgMjQuMTg3IDk2LjcxMDQgMjQuNjI3NEM5Ni4yMzc4IDI1LjA2NzkgOTUuNTk4NiAyNS4yODgxIDk0Ljc5MyAyNS4yODgxQzk0LjM2NjkgMjUuMjg4MSA5My45NDk3IDI1LjE5ODYgOTMuNTQxNSAyNS4wMTk1QzkzLjEzNjkgMjQuODQ0MSA5Mi44Mjg5IDI0LjYxMzEgOTIuNjE3NyAyNC4zMjY3TDkzLjIzNTQgMjMuNTQyNUM5My42MzY0IDI0LjAxODcgOTQuMTMwNSAyNC4yNTY4IDk0LjcxNzggMjQuMjU2OEM5NS4xNTEgMjQuMjU2OCA5NS40OTMgMjQuMTM4NyA5NS43NDM3IDIzLjkwMjNDOTUuOTk0MyAyMy42Njk2IDk2LjExOTYgMjMuMzI1OCA5Ni4xMTk2IDIyLjg3MTFWMjIuNDc5Qzk1Ljc0MzcgMjIuODk3OSA5NS4yNDI0IDIzLjEwNzQgOTQuNjE1NyAyMy4xMDc0QzkzLjkzNTQgMjMuMTA3NCA5My4zNzg2IDIyLjgzNzEgOTIuOTQ1MyAyMi4yOTY0QzkyLjUxNTYgMjEuNzU1NyA5Mi4zMDA4IDIxLjAwNzMgOTIuMzAwOCAyMC4wNTEzWk05My42MDA2IDIwLjE2NDFDOTMuNjAwNiAyMC43NDc3IDkzLjcxODggMjEuMjA3OCA5My45NTUxIDIxLjU0NDRDOTQuMTk1IDIxLjg3NzQgOTQuNTI2MiAyMi4wNDM5IDk0Ljk0ODcgMjIuMDQzOUM5NS40NzUxIDIyLjA0MzkgOTUuODY1NCAyMS44MTg0IDk2LjExOTYgMjEuMzY3MlYxOC44MTA1Qzk1Ljg3MjYgMTguMzcwMSA5NS40ODU4IDE4LjE0OTkgOTQuOTU5NSAxOC4xNDk5Qzk0LjUyOTggMTguMTQ5OSA5NC4xOTUgMTguMzIgOTMuOTU1MSAxOC42NjAyQzkzLjcxODggMTkuMDAwMyA5My42MDA2IDE5LjUwMTYgOTMuNjAwNiAyMC4xNjQxWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTAxLjMzIDIzLjEwNzRDMTAwLjUwMiAyMy4xMDc0IDk5LjgzMTEgMjIuODQ3OCA5OS4zMTU0IDIyLjMyODZDOTguODAzNCAyMS44MDU4IDk4LjU0NzQgMjEuMTExMiA5OC41NDc0IDIwLjI0NDZWMjAuMDgzNUM5OC41NDc0IDE5LjUwMzQgOTguNjU4NCAxOC45ODYgOTguODgwNCAxOC41MzEyQzk5LjEwNiAxOC4wNzI5IDk5LjQyMTEgMTcuNzE2NiA5OS44MjU3IDE3LjQ2MjRDMTAwLjIzIDE3LjIwODIgMTAwLjY4MSAxNy4wODExIDEwMS4xNzkgMTcuMDgxMUMxMDEuOTcxIDE3LjA4MTEgMTAyLjU4MSAxNy4zMzM1IDEwMy4wMTEgMTcuODM4NEMxMDMuNDQ0IDE4LjM0MzMgMTAzLjY2MSAxOS4wNTc2IDEwMy42NjEgMTkuOTgxNFYyMC41MDc4SDk5Ljg2MzNDOTkuOTAyNyAyMC45ODc2IDEwMC4wNjIgMjEuMzY3MiAxMDAuMzQxIDIxLjY0NjVDMTAwLjYyNCAyMS45MjU4IDEwMC45NzkgMjIuMDY1NCAxMDEuNDA1IDIyLjA2NTRDMTAyLjAwMyAyMi4wNjU0IDEwMi40OSAyMS44MjM3IDEwMi44NjYgMjEuMzQwM0wxMDMuNTY5IDIyLjAxMTdDMTAzLjMzNyAyMi4zNTkgMTAzLjAyNSAyMi42Mjk0IDEwMi42MzUgMjIuODIyOEMxMDIuMjQ4IDIzLjAxMjUgMTAxLjgxMyAyMy4xMDc0IDEwMS4zMyAyMy4xMDc0Wk0xMDEuMTc0IDE4LjEyODRDMTAwLjgxNiAxOC4xMjg0IDEwMC41MjYgMTguMjUzNyAxMDAuMzA0IDE4LjUwNDRDMTAwLjA4NSAxOC43NTUgOTkuOTQ1NiAxOS4xMDQyIDk5Ljg4NDggMTkuNTUxOEgxMDIuMzcyVjE5LjQ1NTFDMTAyLjM0MyAxOS4wMTgyIDEwMi4yMjcgMTguNjg4OCAxMDIuMDIyIDE4LjQ2NjhDMTAxLjgxOCAxOC4yNDEyIDEwMS41MzUgMTguMTI4NCAxMDEuMTc0IDE4LjEyODRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xMDkuMDUzIDE1Ljc3NTlWMTcuMTg4NUgxMTAuMDc5VjE4LjE1NTNIMTA5LjA1M1YyMS4zOTk0QzEwOS4wNTMgMjEuNjIxNCAxMDkuMDk2IDIxLjc4MjYgMTA5LjE4MiAyMS44ODI4QzEwOS4yNzIgMjEuOTc5NSAxMDkuNDI5IDIyLjAyNzggMTA5LjY1NSAyMi4wMjc4QzEwOS44MDUgMjIuMDI3OCAxMDkuOTU3IDIyLjAwOTkgMTEwLjExMSAyMS45NzQxVjIyLjk4MzlDMTA5LjgxNCAyMy4wNjYyIDEwOS41MjggMjMuMTA3NCAxMDkuMjUyIDIzLjEwNzRDMTA4LjI0OSAyMy4xMDc0IDEwNy43NDggMjIuNTU0MiAxMDcuNzQ4IDIxLjQ0NzhWMTguMTU1M0gxMDYuNzkyVjE3LjE4ODVIMTA3Ljc0OFYxNS43NzU5SDEwOS4wNTNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xMTMuMTE0IDIxLjEzMDlMMTE0LjI5NSAxNy4xODg1SDExNS42ODdMMTEzLjM3NyAyMy44ODA5QzExMy4wMjIgMjQuODU4NCAxMTIuNDIxIDI1LjM0NzIgMTExLjU3MiAyNS4zNDcyQzExMS4zODIgMjUuMzQ3MiAxMTEuMTczIDI1LjMxNDkgMTEwLjk0NCAyNS4yNTA1VjI0LjI0MDdMMTExLjE5MSAyNC4yNTY4QzExMS41MiAyNC4yNTY4IDExMS43NjcgMjQuMTk2IDExMS45MzIgMjQuMDc0MkMxMTIuMSAyMy45NTYxIDExMi4yMzMgMjMuNzU1NSAxMTIuMzMgMjMuNDcyN0wxMTIuNTE4IDIyLjk3MzFMMTEwLjQ3NyAxNy4xODg1SDExMS44ODRMMTEzLjExNCAyMS4xMzA5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTIxLjUzIDIwLjE1MzNDMTIxLjUzIDIxLjA1MjEgMTIxLjMyNiAyMS43NyAxMjAuOTE4IDIyLjMwNzFDMTIwLjUxIDIyLjg0MDcgMTE5Ljk2MiAyMy4xMDc0IDExOS4yNzQgMjMuMTA3NEMxMTguNjM3IDIzLjEwNzQgMTE4LjEyNyAyMi44OTc5IDExNy43NDQgMjIuNDc5VjI1LjIzNDRIMTE2LjQzOFYxNy4xODg1SDExNy42NDJMMTE3LjY5NSAxNy43NzkzQzExOC4wNzggMTcuMzEzOCAxMTguNTk5IDE3LjA4MTEgMTE5LjI1OCAxNy4wODExQzExOS45NjcgMTcuMDgxMSAxMjAuNTIyIDE3LjM0NiAxMjAuOTIzIDE3Ljg3NkMxMjEuMzI4IDE4LjQwMjMgMTIxLjUzIDE5LjEzNDYgMTIxLjUzIDIwLjA3MjhWMjAuMTUzM1pNMTIwLjIzIDIwLjA0MDVDMTIwLjIzIDE5LjQ2MDQgMTIwLjExNCAxOS4wMDAzIDExOS44ODEgMTguNjYwMkMxMTkuNjUyIDE4LjMyIDExOS4zMjMgMTguMTQ5OSAxMTguODkzIDE4LjE0OTlDMTE4LjM2IDE4LjE0OTkgMTE3Ljk3NiAxOC4zNzAxIDExNy43NDQgMTguODEwNVYyMS4zODg3QzExNy45OCAyMS44Mzk4IDExOC4zNjcgMjIuMDY1NCAxMTguOTA0IDIyLjA2NTRDMTE5LjMxOSAyMi4wNjU0IDExOS42NDMgMjEuODk4OSAxMTkuODc2IDIxLjU2NTlDMTIwLjExMiAyMS4yMjkzIDEyMC4yMyAyMC43MjA5IDEyMC4yMyAyMC4wNDA1WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTI1LjE5OSAyMy4xMDc0QzEyNC4zNzIgMjMuMTA3NCAxMjMuNyAyMi44NDc4IDEyMy4xODUgMjIuMzI4NkMxMjIuNjczIDIxLjgwNTggMTIyLjQxNyAyMS4xMTEyIDEyMi40MTcgMjAuMjQ0NlYyMC4wODM1QzEyMi40MTcgMTkuNTAzNCAxMjIuNTI4IDE4Ljk4NiAxMjIuNzUgMTguNTMxMkMxMjIuOTc1IDE4LjA3MjkgMTIzLjI5IDE3LjcxNjYgMTIzLjY5NSAxNy40NjI0QzEyNC4wOTkgMTcuMjA4MiAxMjQuNTUxIDE3LjA4MTEgMTI1LjA0OCAxNy4wODExQzEyNS44NCAxNy4wODExIDEyNi40NSAxNy4zMzM1IDEyNi44OCAxNy44Mzg0QzEyNy4zMTMgMTguMzQzMyAxMjcuNTMgMTkuMDU3NiAxMjcuNTMgMTkuOTgxNFYyMC41MDc4SDEyMy43MzJDMTIzLjc3MiAyMC45ODc2IDEyMy45MzEgMjEuMzY3MiAxMjQuMjEgMjEuNjQ2NUMxMjQuNDkzIDIxLjkyNTggMTI0Ljg0OCAyMi4wNjU0IDEyNS4yNzQgMjIuMDY1NEMxMjUuODcyIDIyLjA2NTQgMTI2LjM1OSAyMS44MjM3IDEyNi43MzUgMjEuMzQwM0wxMjcuNDM4IDIyLjAxMTdDMTI3LjIwNiAyMi4zNTkgMTI2Ljg5NCAyMi42Mjk0IDEyNi41MDQgMjIuODIyOEMxMjYuMTE3IDIzLjAxMjUgMTI1LjY4MiAyMy4xMDc0IDEyNS4xOTkgMjMuMTA3NFpNMTI1LjA0MyAxOC4xMjg0QzEyNC42ODUgMTguMTI4NCAxMjQuMzk1IDE4LjI1MzcgMTI0LjE3MyAxOC41MDQ0QzEyMy45NTQgMTguNzU1IDEyMy44MTUgMTkuMTA0MiAxMjMuNzU0IDE5LjU1MThIMTI2LjI0MVYxOS40NTUxQzEyNi4yMTIgMTkuMDE4MiAxMjYuMDk2IDE4LjY4ODggMTI1Ljg5MiAxOC40NjY4QzEyNS42ODggMTguMjQxMiAxMjUuNDA1IDE4LjEyODQgMTI1LjA0MyAxOC4xMjg0WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTQ0Ljg4MiAyMC45ODU4QzE0NC44ODIgMjAuNjQyMSAxNDQuNzYxIDIwLjM3NzEgMTQ0LjUxNyAyMC4xOTA5QzE0NC4yNzcgMjAuMDA0NyAxNDMuODQyIDE5LjgxNjcgMTQzLjIxMiAxOS42MjdDMTQyLjU4MiAxOS40MzcyIDE0Mi4wOCAxOS4yMjU5IDE0MS43MDggMTguOTkzMkMxNDAuOTk1IDE4LjU0NTYgMTQwLjYzOSAxNy45NjE5IDE0MC42MzkgMTcuMjQyMkMxNDAuNjM5IDE2LjYxMiAxNDAuODk1IDE2LjA5MjggMTQxLjQwNyAxNS42ODQ2QzE0MS45MjMgMTUuMjc2NCAxNDIuNTkxIDE1LjA3MjMgMTQzLjQxMSAxNS4wNzIzQzE0My45NTUgMTUuMDcyMyAxNDQuNDQgMTUuMTcyNSAxNDQuODY2IDE1LjM3M0MxNDUuMjkyIDE1LjU3MzYgMTQ1LjYyNyAxNS44NiAxNDUuODcxIDE2LjIzMjRDMTQ2LjExNCAxNi42MDEyIDE0Ni4yMzYgMTcuMDExMiAxNDYuMjM2IDE3LjQ2MjRIMTQ0Ljg4MkMxNDQuODgyIDE3LjA1NDIgMTQ0Ljc1MyAxNi43MzU1IDE0NC40OTYgMTYuNTA2M0MxNDQuMjQxIDE2LjI3MzYgMTQzLjg3NiAxNi4xNTcyIDE0My40IDE2LjE1NzJDMTQyLjk1NiAxNi4xNTcyIDE0Mi42MSAxNi4yNTIxIDE0Mi4zNjMgMTYuNDQxOUMxNDIuMTIgMTYuNjMxNyAxNDEuOTk4IDE2Ljg5NjYgMTQxLjk5OCAxNy4yMzY4QzE0MS45OTggMTcuNTIzMyAxNDIuMTMxIDE3Ljc2MzIgMTQyLjM5NiAxNy45NTY1QzE0Mi42NiAxOC4xNDYzIDE0My4wOTcgMTguMzMyNSAxNDMuNzA2IDE4LjUxNTFDMTQ0LjMxNSAxOC42OTQyIDE0NC44MDQgMTguOTAwMSAxNDUuMTcyIDE5LjEzMjhDMTQ1LjU0MSAxOS4zNjIgMTQ1LjgxMiAxOS42MjcgMTQ1Ljk4MyAxOS45Mjc3QzE0Ni4xNTUgMjAuMjI0OSAxNDYuMjQxIDIwLjU3NDEgMTQ2LjI0MSAyMC45NzUxQzE0Ni4yNDEgMjEuNjI2OCAxNDUuOTkxIDIyLjE0NiAxNDUuNDg5IDIyLjUzMjdDMTQ0Ljk5MiAyMi45MTU5IDE0NC4zMTUgMjMuMTA3NCAxNDMuNDU5IDIzLjEwNzRDMTQyLjg5MyAyMy4xMDc0IDE0Mi4zNzIgMjMuMDAzNiAxNDEuODk2IDIyLjc5NTlDMTQxLjQyMyAyMi41ODQ2IDE0MS4wNTUgMjIuMjk0NiAxNDAuNzkgMjEuOTI1OEMxNDAuNTI4IDIxLjU1NyAxNDAuMzk3IDIxLjEyNzMgMTQwLjM5NyAyMC42MzY3SDE0MS43NTZDMTQxLjc1NiAyMS4wODA3IDE0MS45MDMgMjEuNDI0NSAxNDIuMTk3IDIxLjY2OEMxNDIuNDkgMjEuOTExNSAxNDIuOTExIDIyLjAzMzIgMTQzLjQ1OSAyMi4wMzMyQzE0My45MzIgMjIuMDMzMiAxNDQuMjg2IDIxLjkzODMgMTQ0LjUyMiAyMS43NDg1QzE0NC43NjIgMjEuNTU1MiAxNDQuODgyIDIxLjMwMDkgMTQ0Ljg4MiAyMC45ODU4WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTQ4Ljk0MyAxNS43NzU5VjE3LjE4ODVIMTQ5Ljk2OVYxOC4xNTUzSDE0OC45NDNWMjEuMzk5NEMxNDguOTQzIDIxLjYyMTQgMTQ4Ljk4NiAyMS43ODI2IDE0OS4wNzIgMjEuODgyOEMxNDkuMTYxIDIxLjk3OTUgMTQ5LjMxOSAyMi4wMjc4IDE0OS41NDQgMjIuMDI3OEMxNDkuNjk1IDIyLjAyNzggMTQ5Ljg0NyAyMi4wMDk5IDE1MC4wMDEgMjEuOTc0MVYyMi45ODM5QzE0OS43MDQgMjMuMDY2MiAxNDkuNDE3IDIzLjEwNzQgMTQ5LjE0MiAyMy4xMDc0QzE0OC4xMzkgMjMuMTA3NCAxNDcuNjM4IDIyLjU1NDIgMTQ3LjYzOCAyMS40NDc4VjE4LjE1NTNIMTQ2LjY4MlYxNy4xODg1SDE0Ny42MzhWMTUuNzc1OUgxNDguOTQzWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTU0LjQ0MyAyM0MxNTQuMzg2IDIyLjg4OSAxNTQuMzM1IDIyLjcwODIgMTU0LjI5MiAyMi40NTc1QzE1My44NzcgMjIuODkwOCAxNTMuMzY5IDIzLjEwNzQgMTUyLjc2NyAyMy4xMDc0QzE1Mi4xODMgMjMuMTA3NCAxNTEuNzA3IDIyLjk0MDkgMTUxLjMzOCAyMi42MDc5QzE1MC45NyAyMi4yNzQ5IDE1MC43ODUgMjEuODYzMSAxNTAuNzg1IDIxLjM3MjZDMTUwLjc4NSAyMC43NTMxIDE1MS4wMTQgMjAuMjc4NiAxNTEuNDczIDE5Ljk0OTJDMTUxLjkzNSAxOS42MTYyIDE1Mi41OTMgMTkuNDQ5NyAxNTMuNDQ5IDE5LjQ0OTdIMTU0LjI1VjE5LjA2ODRDMTU0LjI1IDE4Ljc2NzYgMTU0LjE2NSAxOC41Mjc3IDE1My45OTcgMTguMzQ4NkMxNTMuODI5IDE4LjE2NiAxNTMuNTczIDE4LjA3NDcgMTUzLjIyOSAxOC4wNzQ3QzE1Mi45MzIgMTguMDc0NyAxNTIuNjg4IDE4LjE0OTkgMTUyLjQ5OSAxOC4zMDAzQzE1Mi4zMDkgMTguNDQ3MSAxNTIuMjE0IDE4LjYzNTEgMTUyLjIxNCAxOC44NjQzSDE1MC45MDlDMTUwLjkwOSAxOC41NDU2IDE1MS4wMTQgMTguMjQ4NCAxNTEuMjI2IDE3Ljk3MjdDMTUxLjQzNyAxNy42OTM0IDE1MS43MjMgMTcuNDc0OSAxNTIuMDg1IDE3LjMxNzRDMTUyLjQ1IDE3LjE1OTggMTUyLjg1NyAxNy4wODExIDE1My4zMDQgMTcuMDgxMUMxNTMuOTg1IDE3LjA4MTEgMTU0LjUyNyAxNy4yNTI5IDE1NC45MzIgMTcuNTk2N0MxNTUuMzM2IDE3LjkzNjggMTU1LjU0NCAxOC40MTY3IDE1NS41NTUgMTkuMDM2MVYyMS42NTcyQzE1NS41NTUgMjIuMTggMTU1LjYyOCAyMi41OTcyIDE1NS43NzUgMjIuOTA4N1YyM0gxNTQuNDQzWk0xNTMuMDA5IDIyLjA2MDFDMTUzLjI2NyAyMi4wNjAxIDE1My41MDggMjEuOTk3NCAxNTMuNzM0IDIxLjg3MjFDMTUzLjk2MyAyMS43NDY3IDE1NC4xMzUgMjEuNTc4NSAxNTQuMjUgMjEuMzY3MlYyMC4yNzE1SDE1My41NDZDMTUzLjA2MiAyMC4yNzE1IDE1Mi42OTkgMjAuMzU1NiAxNTIuNDU2IDIwLjUyMzlDMTUyLjIxMiAyMC42OTIyIDE1Mi4wOSAyMC45MzAzIDE1Mi4wOSAyMS4yMzgzQzE1Mi4wOSAyMS40ODg5IDE1Mi4xNzMgMjEuNjg5NSAxNTIuMzM3IDIxLjgzOThDMTUyLjUwNiAyMS45ODY3IDE1Mi43MjkgMjIuMDYwMSAxNTMuMDA5IDIyLjA2MDFaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xNTguNTU3IDE1Ljc3NTlWMTcuMTg4NUgxNTkuNTgzVjE4LjE1NTNIMTU4LjU1N1YyMS4zOTk0QzE1OC41NTcgMjEuNjIxNCAxNTguNiAyMS43ODI2IDE1OC42ODYgMjEuODgyOEMxNTguNzc2IDIxLjk3OTUgMTU4LjkzMyAyMi4wMjc4IDE1OS4xNTkgMjIuMDI3OEMxNTkuMzA5IDIyLjAyNzggMTU5LjQ2MSAyMi4wMDk5IDE1OS42MTUgMjEuOTc0MVYyMi45ODM5QzE1OS4zMTggMjMuMDY2MiAxNTkuMDMyIDIzLjEwNzQgMTU4Ljc1NiAyMy4xMDc0QzE1Ny43NTMgMjMuMTA3NCAxNTcuMjUyIDIyLjU1NDIgMTU3LjI1MiAyMS40NDc4VjE4LjE1NTNIMTU2LjI5NlYxNy4xODg1SDE1Ny4yNTJWMTUuNzc1OUgxNTguNTU3WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTY0LjEwNSAyMi40MzA3QzE2My43MjIgMjIuODgxOCAxNjMuMTc4IDIzLjEwNzQgMTYyLjQ3MyAyMy4xMDc0QzE2MS44NDIgMjMuMTA3NCAxNjEuMzY0IDIyLjkyMyAxNjEuMDM5IDIyLjU1NDJDMTYwLjcxNiAyMi4xODU0IDE2MC41NTUgMjEuNjUxOSAxNjAuNTU1IDIwLjk1MzZWMTcuMTg4NUgxNjEuODZWMjAuOTM3NUMxNjEuODYgMjEuNjc1MSAxNjIuMTY3IDIyLjA0MzkgMTYyLjc3OSAyMi4wNDM5QzE2My40MTMgMjIuMDQzOSAxNjMuODQgMjEuODE2NiAxNjQuMDYyIDIxLjM2MThWMTcuMTg4NUgxNjUuMzY4VjIzSDE2NC4xMzhMMTY0LjEwNSAyMi40MzA3WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTY5Ljk1NSAyMS40MjA5QzE2OS45NTUgMjEuMTg4MiAxNjkuODU4IDIxLjAxMDkgMTY5LjY2NSAyMC44ODkyQzE2OS40NzUgMjAuNzY3NCAxNjkuMTU4IDIwLjY2IDE2OC43MTQgMjAuNTY2OUMxNjguMjcgMjAuNDczOCAxNjcuODk5IDIwLjM1NTYgMTY3LjYwMiAyMC4yMTI0QzE2Ni45NSAxOS44OTczIDE2Ni42MjUgMTkuNDQwOCAxNjYuNjI1IDE4Ljg0MjhDMTY2LjYyNSAxOC4zNDE1IDE2Ni44MzYgMTcuOTIyNSAxNjcuMjU4IDE3LjU4NTlDMTY3LjY4MSAxNy4yNDkzIDE2OC4yMTggMTcuMDgxMSAxNjguODcgMTcuMDgxMUMxNjkuNTY0IDE3LjA4MTEgMTcwLjEyNSAxNy4yNTI5IDE3MC41NTEgMTcuNTk2N0MxNzAuOTggMTcuOTQwNCAxNzEuMTk1IDE4LjM4NjIgMTcxLjE5NSAxOC45MzQxSDE2OS44OUMxNjkuODkgMTguNjgzNCAxNjkuNzk3IDE4LjQ3NTcgMTY5LjYxMSAxOC4zMTFDMTY5LjQyNSAxOC4xNDI3IDE2OS4xNzggMTguMDU4NiAxNjguODcgMTguMDU4NkMxNjguNTgzIDE4LjA1ODYgMTY4LjM0OSAxOC4xMjQ4IDE2OC4xNjYgMTguMjU3M0MxNjcuOTg3IDE4LjM4OTggMTY3Ljg5NyAxOC41NjcxIDE2Ny44OTcgMTguNzg5MUMxNjcuODk3IDE4Ljk4OTYgMTY3Ljk4MiAxOS4xNDUzIDE2OC4xNSAxOS4yNTYzQzE2OC4zMTggMTkuMzY3NCAxNjguNjU4IDE5LjQ4MDEgMTY5LjE3IDE5LjU5NDdDMTY5LjY4MiAxOS43MDU3IDE3MC4wODMgMTkuODQgMTcwLjM3NCAxOS45OTc2QzE3MC42NjcgMjAuMTUxNSAxNzAuODg0IDIwLjMzNzcgMTcxLjAyMyAyMC41NTYyQzE3MS4xNjcgMjAuNzc0NiAxNzEuMjM4IDIxLjAzOTYgMTcxLjIzOCAyMS4zNTExQzE3MS4yMzggMjEuODczOSAxNzEuMDIyIDIyLjI5ODIgMTcwLjU4OCAyMi42MjRDMTcwLjE1NSAyMi45NDYzIDE2OS41ODggMjMuMTA3NCAxNjguODg2IDIzLjEwNzRDMTY4LjQxIDIzLjEwNzQgMTY3Ljk4NSAyMy4wMjE1IDE2Ny42MTMgMjIuODQ5NkMxNjcuMjQgMjIuNjc3NyAxNjYuOTUgMjIuNDQxNCAxNjYuNzQzIDIyLjE0MDZDMTY2LjUzNSAyMS44Mzk4IDE2Ni40MzEgMjEuNTE1OCAxNjYuNDMxIDIxLjE2ODVIMTY3LjY5OUMxNjcuNzE3IDIxLjQ3NjQgMTY3LjgzMyAyMS43MTQ1IDE2OC4wNDggMjEuODgyOEMxNjguMjYzIDIyLjA0NzUgMTY4LjU0NyAyMi4xMjk5IDE2OC45MDIgMjIuMTI5OUMxNjkuMjQ2IDIyLjEyOTkgMTY5LjUwNyAyMi4wNjU0IDE2OS42ODYgMjEuOTM2NUMxNjkuODY1IDIxLjgwNCAxNjkuOTU1IDIxLjYzMjIgMTY5Ljk1NSAyMS40MjA5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTIuNzczNCA2My40NzlDMTIuNzczNCA2My4zMDcxIDEyLjgyMzYgNjMuMTYzOSAxMi45MjM4IDYzLjA0OTNDMTMuMDI3NyA2Mi45MzQ3IDEzLjE4MTYgNjIuODc3NCAxMy4zODU3IDYyLjg3NzRDMTMuNTg5OCA2Mi44Nzc0IDEzLjc0MzggNjIuOTM0NyAxMy44NDc3IDYzLjA0OTNDMTMuOTU1MSA2My4xNjM5IDE0LjAwODggNjMuMzA3MSAxNC4wMDg4IDYzLjQ3OUMxNC4wMDg4IDYzLjY0MzcgMTMuOTU1MSA2My43ODE2IDEzLjg0NzcgNjMuODkyNkMxMy43NDM4IDY0LjAwMzYgMTMuNTg5OCA2NC4wNTkxIDEzLjM4NTcgNjQuMDU5MUMxMy4xODE2IDY0LjA1OTEgMTMuMDI3NyA2NC4wMDM2IDEyLjkyMzggNjMuODkyNkMxMi44MjM2IDYzLjc4MTYgMTIuNzczNCA2My42NDM3IDEyLjc3MzQgNjMuNDc5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTUuNjczOCA2My40NzlDMTUuNjczOCA2My4zMDcxIDE1LjcyNCA2My4xNjM5IDE1LjgyNDIgNjMuMDQ5M0MxNS45MjgxIDYyLjkzNDcgMTYuMDgyIDYyLjg3NzQgMTYuMjg2MSA2Mi44Nzc0QzE2LjQ5MDIgNjIuODc3NCAxNi42NDQyIDYyLjkzNDcgMTYuNzQ4IDYzLjA0OTNDMTYuODU1NSA2My4xNjM5IDE2LjkwOTIgNjMuMzA3MSAxNi45MDkyIDYzLjQ3OUMxNi45MDkyIDYzLjY0MzcgMTYuODU1NSA2My43ODE2IDE2Ljc0OCA2My44OTI2QzE2LjY0NDIgNjQuMDAzNiAxNi40OTAyIDY0LjA1OTEgMTYuMjg2MSA2NC4wNTkxQzE2LjA4MiA2NC4wNTkxIDE1LjkyODEgNjQuMDAzNiAxNS44MjQyIDYzLjg5MjZDMTUuNzI0IDYzLjc4MTYgMTUuNjczOCA2My42NDM3IDE1LjY3MzggNjMuNDc5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMjAuODgzOCA2My4yOTY0QzIxLjIzODMgNjMuMjk2NCAyMS41NDggNjMuMTg5IDIxLjgxMyA2Mi45NzQxQzIyLjA3OCA2Mi43NTkzIDIyLjIyNDggNjIuNDkwNyAyMi4yNTM0IDYyLjE2ODVIMjMuMTkzNEMyMy4xNzU1IDYyLjUwMTUgMjMuMDYwOSA2Mi44MTg0IDIyLjg0OTYgNjMuMTE5MUMyMi42MzgzIDYzLjQxOTkgMjIuMzU1NSA2My42NTk4IDIyLjAwMSA2My44Mzg5QzIxLjY1MDEgNjQuMDE3OSAyMS4yNzc3IDY0LjEwNzQgMjAuODgzOCA2NC4xMDc0QzIwLjA5MjQgNjQuMTA3NCAxOS40NjIyIDYzLjg0NDIgMTguOTkzMiA2My4zMTc5QzE4LjUyNzcgNjIuNzg3OSAxOC4yOTQ5IDYyLjA2NDYgMTguMjk0OSA2MS4xNDc5VjYwLjk4MTRDMTguMjk0OSA2MC40MTU3IDE4LjM5ODggNTkuOTEyNiAxOC42MDY0IDU5LjQ3MjJDMTguODE0MSA1OS4wMzE3IDE5LjExMTMgNTguNjg5OCAxOS40OTggNTguNDQ2M0MxOS44ODgzIDU4LjIwMjggMjAuMzQ4NSA1OC4wODExIDIwLjg3ODQgNTguMDgxMUMyMS41MzAxIDU4LjA4MTEgMjIuMDcwOCA1OC4yNzYyIDIyLjUwMDUgNTguNjY2NUMyMi45MzM4IDU5LjA1NjggMjMuMTY0NyA1OS41NjM1IDIzLjE5MzQgNjAuMTg2NUgyMi4yNTM0QzIyLjIyNDggNTkuODEwNSAyMi4wODE1IDU5LjUwMjYgMjEuODIzNyA1OS4yNjI3QzIxLjU2OTUgNTkuMDE5MiAyMS4yNTQ0IDU4Ljg5NzUgMjAuODc4NCA1OC44OTc1QzIwLjM3MzUgNTguODk3NSAxOS45ODE0IDU5LjA4MDEgMTkuNzAyMSA1OS40NDUzQzE5LjQyNjQgNTkuODA3IDE5LjI4ODYgNjAuMzMxNSAxOS4yODg2IDYxLjAxOVY2MS4yMDdDMTkuMjg4NiA2MS44NzY2IDE5LjQyNjQgNjIuMzkyMyAxOS43MDIxIDYyLjc1MzlDMTkuOTc3OSA2My4xMTU2IDIwLjM3MTcgNjMuMjk2NCAyMC44ODM4IDYzLjI5NjRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0yNC4wNDc0IDYxLjA0MDVDMjQuMDQ3NCA2MC40NzEyIDI0LjE1ODQgNTkuOTU5MSAyNC4zODA0IDU5LjUwNDRDMjQuNjA2IDU5LjA0OTYgMjQuOTE3NSA1OC42OTg3IDI1LjMxNDkgNTguNDUxN0MyNS43MTYgNTguMjA0NiAyNi4xNzI1IDU4LjA4MTEgMjYuNjg0NiA1OC4wODExQzI3LjQ3NTkgNTguMDgxMSAyOC4xMTUxIDU4LjM1NSAyOC42MDIxIDU4LjkwMjhDMjkuMDkyNiA1OS40NTA3IDI5LjMzNzkgNjAuMTc5NCAyOS4zMzc5IDYxLjA4ODlWNjEuMTU4N0MyOS4zMzc5IDYxLjcyNDQgMjkuMjI4NyA2Mi4yMzI5IDI5LjAxMDMgNjIuNjg0MUMyOC43OTU0IDYzLjEzMTcgMjguNDg1NyA2My40ODA4IDI4LjA4MTEgNjMuNzMxNEMyNy42OCA2My45ODIxIDI3LjIxODEgNjQuMTA3NCAyNi42OTUzIDY0LjEwNzRDMjUuOTA3NiA2NC4xMDc0IDI1LjI2ODQgNjMuODMzNSAyNC43Nzc4IDYzLjI4NTZDMjQuMjkwOSA2Mi43Mzc4IDI0LjA0NzQgNjIuMDEyNyAyNC4wNDc0IDYxLjExMDRWNjEuMDQwNVpNMjUuMDQ2NCA2MS4xNTg3QzI1LjA0NjQgNjEuODAzMiAyNS4xOTUgNjIuMzIwNiAyNS40OTIyIDYyLjcxMDlDMjUuNzkzIDYzLjEwMTIgMjYuMTk0IDYzLjI5NjQgMjYuNjk1MyA2My4yOTY0QzI3LjIwMDIgNjMuMjk2NCAyNy42MDEyIDYzLjA5OTQgMjcuODk4NCA2Mi43MDU2QzI4LjE5NTYgNjIuMzA4MSAyOC4zNDQyIDYxLjc1MzEgMjguMzQ0MiA2MS4wNDA1QzI4LjM0NDIgNjAuNDAzMiAyOC4xOTIxIDU5Ljg4NzUgMjcuODg3NyA1OS40OTM3QzI3LjU4NjkgNTkuMDk2MiAyNy4xODU5IDU4Ljg5NzUgMjYuNjg0NiA1OC44OTc1QzI2LjE5NCA1OC44OTc1IDI1Ljc5ODMgNTkuMDkyNiAyNS40OTc2IDU5LjQ4MjlDMjUuMTk2OCA1OS44NzMyIDI1LjA0NjQgNjAuNDMxOCAyNS4wNDY0IDYxLjE1ODdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0zNC42NzY4IDYxLjM3MzVIMzUuNzYxN1Y2Mi4xODQ2SDM0LjY3NjhWNjRIMzMuNjc3N1Y2Mi4xODQ2SDMwLjExNjdWNjEuNTk5MUwzMy42MTg3IDU2LjE3OTdIMzQuNjc2OFY2MS4zNzM1Wk0zMS4yNDQ2IDYxLjM3MzVIMzMuNjc3N1Y1Ny41Mzg2TDMzLjU1OTYgNTcuNzUzNEwzMS4yNDQ2IDYxLjM3MzVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0zNy43ODY2IDU4LjE4ODVWNjQuNjcxNEMzNy43ODY2IDY1Ljc4ODYgMzcuMjc5OSA2Ni4zNDcyIDM2LjI2NjYgNjYuMzQ3MkMzNi4wNDgyIDY2LjM0NzIgMzUuODQ1OSA2Ni4zMTQ5IDM1LjY1OTcgNjYuMjUwNVY2NS40NTU2QzM1Ljc3NDMgNjUuNDg0MiAzNS45MjQ2IDY1LjQ5ODUgMzYuMTEwOCA2NS40OTg1QzM2LjMzMjggNjUuNDk4NSAzNi41MDExIDY1LjQzNzcgMzYuNjE1NyA2NS4zMTU5QzM2LjczMzkgNjUuMTk3OCAzNi43OTMgNjQuOTkwMSAzNi43OTMgNjQuNjkyOVY1OC4xODg1SDM3Ljc4NjZaTTM2LjY5MDkgNTYuNjQ3QzM2LjY5MDkgNTYuNDg5NCAzNi43MzkzIDU2LjM1NTEgMzYuODM1OSA1Ni4yNDQxQzM2LjkzNjIgNTYuMTI5NiAzNy4wODEyIDU2LjA3MjMgMzcuMjcxIDU2LjA3MjNDMzcuNDY0NCA1Ni4wNzIzIDM3LjYxMTIgNTYuMTI3OCAzNy43MTE0IDU2LjIzODhDMzcuODExNyA1Ni4zNDk4IDM3Ljg2MTggNTYuNDg1OCAzNy44NjE4IDU2LjY0N0MzNy44NjE4IDU2LjgwODEgMzcuODExNyA1Ni45NDI0IDM3LjcxMTQgNTcuMDQ5OEMzNy42MTEyIDU3LjE1NzIgMzcuNDY0NCA1Ny4yMTA5IDM3LjI3MSA1Ny4yMTA5QzM3LjA3NzYgNTcuMjEwOSAzNi45MzI2IDU3LjE1NzIgMzYuODM1OSA1Ny4wNDk4QzM2LjczOTMgNTYuOTQyNCAzNi42OTA5IDU2LjgwODEgMzYuNjkwOSA1Ni42NDdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik00Mi45ODA1IDYzLjQyNTNDNDIuNTkzOCA2My44OCA0Mi4wMjYyIDY0LjEwNzQgNDEuMjc3OCA2NC4xMDc0QzQwLjY1ODQgNjQuMTA3NCA0MC4xODU3IDYzLjkyODQgMzkuODU5OSA2My41NzAzQzM5LjUzNzYgNjMuMjA4NyAzOS4zNzQ3IDYyLjY3NTEgMzkuMzcxMSA2MS45Njk3VjU4LjE4ODVINDAuMzY0N1Y2MS45NDI5QzQwLjM2NDcgNjIuODIzNyA0MC43MjI4IDYzLjI2NDIgNDEuNDM5IDYzLjI2NDJDNDIuMTk4MSA2My4yNjQyIDQyLjcwMyA2Mi45ODEzIDQyLjk1MzYgNjIuNDE1NVY1OC4xODg1SDQzLjk0NzNWNjRINDMuMDAyTDQyLjk4MDUgNjMuNDI1M1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTY1LjMxMSA1Ny4wMjgzSDYyLjc5NzRWNjRINjEuNzcxNVY1Ny4wMjgzSDU5LjI2MzJWNTYuMTc5N0g2NS4zMTFWNTcuMDI4M1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTcxLjE0OTQgNjIuNjMwNEw3Mi4yNjY2IDU4LjE4ODVINzMuMjYwM0w3MS41Njg0IDY0SDcwLjc2MjdMNjkuMzUwMSA1OS41OTU3TDY3Ljk3NTEgNjRINjcuMTY5NEw2NS40ODI5IDU4LjE4ODVINjYuNDcxMkw2Ny42MTUyIDYyLjUzOTFMNjguOTY4OCA1OC4xODg1SDY5Ljc2OUw3MS4xNDk0IDYyLjYzMDRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik03NC4wMTIyIDYxLjA0MDVDNzQuMDEyMiA2MC40NzEyIDc0LjEyMzIgNTkuOTU5MSA3NC4zNDUyIDU5LjUwNDRDNzQuNTcwOCA1OS4wNDk2IDc0Ljg4MjMgNTguNjk4NyA3NS4yNzk4IDU4LjQ1MTdDNzUuNjgwOCA1OC4yMDQ2IDc2LjEzNzQgNTguMDgxMSA3Ni42NDk0IDU4LjA4MTFDNzcuNDQwOCA1OC4wODExIDc4LjA3OTkgNTguMzU1IDc4LjU2NjkgNTguOTAyOEM3OS4wNTc1IDU5LjQ1MDcgNzkuMzAyNyA2MC4xNzk0IDc5LjMwMjcgNjEuMDg4OVY2MS4xNTg3Qzc5LjMwMjcgNjEuNzI0NCA3OS4xOTM1IDYyLjIzMjkgNzguOTc1MSA2Mi42ODQxQzc4Ljc2MDMgNjMuMTMxNyA3OC40NTA1IDYzLjQ4MDggNzguMDQ1OSA2My43MzE0Qzc3LjY0NDkgNjMuOTgyMSA3Ny4xODI5IDY0LjEwNzQgNzYuNjYwMiA2NC4xMDc0Qzc1Ljg3MjQgNjQuMTA3NCA3NS4yMzMyIDYzLjgzMzUgNzQuNzQyNyA2My4yODU2Qzc0LjI1NTcgNjIuNzM3OCA3NC4wMTIyIDYyLjAxMjcgNzQuMDEyMiA2MS4xMTA0VjYxLjA0MDVaTTc1LjAxMTIgNjEuMTU4N0M3NS4wMTEyIDYxLjgwMzIgNzUuMTU5OCA2Mi4zMjA2IDc1LjQ1NyA2Mi43MTA5Qzc1Ljc1NzggNjMuMTAxMiA3Ni4xNTg5IDYzLjI5NjQgNzYuNjYwMiA2My4yOTY0Qzc3LjE2NSA2My4yOTY0IDc3LjU2NjEgNjMuMDk5NCA3Ny44NjMzIDYyLjcwNTZDNzguMTYwNSA2Mi4zMDgxIDc4LjMwOTEgNjEuNzUzMSA3OC4zMDkxIDYxLjA0MDVDNzguMzA5MSA2MC40MDMyIDc4LjE1NjkgNTkuODg3NSA3Ny44NTI1IDU5LjQ5MzdDNzcuNTUxOCA1OS4wOTYyIDc3LjE1MDcgNTguODk3NSA3Ni42NDk0IDU4Ljg5NzVDNzYuMTU4OSA1OC44OTc1IDc1Ljc2MzIgNTkuMDkyNiA3NS40NjI0IDU5LjQ4MjlDNzUuMTYxNiA1OS44NzMyIDc1LjAxMTIgNjAuNDMxOCA3NS4wMTEyIDYxLjE1ODdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik04Mi42MTY3IDYxLjA4MzVINzkuOTk1NlY2MC4yNzI1SDgyLjYxNjdWNjEuMDgzNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTg4LjczNDQgNjIuNjMwNEw4OS44NTE2IDU4LjE4ODVIOTAuODQ1Mkw4OS4xNTMzIDY0SDg4LjM0NzdMODYuOTM1MSA1OS41OTU3TDg1LjU2MDEgNjRIODQuNzU0NEw4My4wNjc5IDU4LjE4ODVIODQuMDU2Mkw4NS4yMDAyIDYyLjUzOTFMODYuNTUzNyA1OC4xODg1SDg3LjM1NEw4OC43MzQ0IDYyLjYzMDRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik05NS40NDgyIDY0Qzk1LjM5MSA2My44ODU0IDk1LjM0NDQgNjMuNjgxMyA5NS4zMDg2IDYzLjM4NzdDOTQuODQ2NyA2My44Njc1IDk0LjI5NTIgNjQuMTA3NCA5My42NTQzIDY0LjEwNzRDOTMuMDgxNCA2NC4xMDc0IDkyLjYxMDUgNjMuOTQ2MyA5Mi4yNDE3IDYzLjYyNEM5MS44NzY1IDYzLjI5ODIgOTEuNjkzOCA2Mi44ODY0IDkxLjY5MzggNjIuMzg4N0M5MS42OTM4IDYxLjc4MzUgOTEuOTIzIDYxLjMxNDUgOTIuMzgxMyA2MC45ODE0QzkyLjg0MzMgNjAuNjQ0OSA5My40OTE0IDYwLjQ3NjYgOTQuMzI1NyA2MC40NzY2SDk1LjI5MjVWNjAuMDJDOTUuMjkyNSA1OS42NzI3IDk1LjE4ODYgNTkuMzk3IDk0Ljk4MSA1OS4xOTI5Qzk0Ljc3MzMgNTguOTg1MiA5NC40NjcxIDU4Ljg4MTMgOTQuMDYyNSA1OC44ODEzQzkzLjcwOCA1OC44ODEzIDkzLjQxMDggNTguOTcwOSA5My4xNzA5IDU5LjE0OTlDOTIuOTMxIDU5LjMyODkgOTIuODExIDU5LjU0NTYgOTIuODExIDU5Ljc5OThIOTEuODEyQzkxLjgxMiA1OS41MDk4IDkxLjkxNDEgNTkuMjMwNSA5Mi4xMTgyIDU4Ljk2MTlDOTIuMzI1OCA1OC42ODk4IDkyLjYwNTEgNTguNDc0OSA5Mi45NTYxIDU4LjMxNzRDOTMuMzEwNSA1OC4xNTk4IDkzLjY5OTEgNTguMDgxMSA5NC4xMjE2IDU4LjA4MTFDOTQuNzkxMiA1OC4wODExIDk1LjMxNTggNTguMjQ5MyA5NS42OTUzIDU4LjU4NTlDOTYuMDc0OSA1OC45MTg5IDk2LjI3MTggNTkuMzc5MSA5Ni4yODYxIDU5Ljk2NjNWNjIuNjQxMUM5Ni4yODYxIDYzLjE3NDYgOTYuMzU0MiA2My41OTkgOTYuNDkwMiA2My45MTQxVjY0SDk1LjQ0ODJaTTkzLjc5OTMgNjMuMjQyN0M5NC4xMTA4IDYzLjI0MjcgOTQuNDA2MiA2My4xNjIxIDk0LjY4NTUgNjMuMDAxQzk0Ljk2NDggNjIuODM5OCA5NS4xNjcyIDYyLjYzMDQgOTUuMjkyNSA2Mi4zNzI2VjYxLjE4MDJIOTQuNTEzN0M5My4yOTYyIDYxLjE4MDIgOTIuNjg3NSA2MS41MzY1IDkyLjY4NzUgNjIuMjQ5QzkyLjY4NzUgNjIuNTYwNSA5Mi43OTEzIDYyLjgwNCA5Mi45OTkgNjIuOTc5NUM5My4yMDY3IDYzLjE1NDkgOTMuNDczNSA2My4yNDI3IDkzLjc5OTMgNjMuMjQyN1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTk5LjY1OTIgNjIuNTQ0NEwxMDEuMDEzIDU4LjE4ODVIMTAyLjA3Nkw5OS43Mzk3IDY0Ljg5N0M5OS4zNzgxIDY1Ljg2MzggOTguODAzNCA2Ni4zNDcyIDk4LjAxNTYgNjYuMzQ3Mkw5Ny44Mjc2IDY2LjMzMTFMOTcuNDU3IDY2LjI2MTJWNjUuNDU1Nkw5Ny43MjU2IDY1LjQ3NzFDOTguMDYyMiA2NS40NzcxIDk4LjMyMzYgNjUuNDA5IDk4LjUwOTggNjUuMjcyOUM5OC42OTk1IDY1LjEzNjkgOTguODU1MyA2NC44ODggOTguOTc3MSA2NC41MjY0TDk5LjE5NzMgNjMuOTM1NUw5Ny4xMjQgNTguMTg4NUg5OC4yMDlMOTkuNjU5MiA2Mi41NDQ0WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNjUuOTIzMyAxMDAuMzQyQzY1LjkyMzMgMTAxLjEwOSA2NS43OTQ0IDEwMS43NzggNjUuNTM2NiAxMDIuMzUxQzY1LjI3ODggMTAyLjkyIDY0LjkxMzYgMTAzLjM1NSA2NC40NDA5IDEwMy42NTZDNjMuOTY4MyAxMDMuOTU3IDYzLjQxNjggMTA0LjEwNyA2Mi43ODY2IDEwNC4xMDdDNjIuMTcwNyAxMDQuMTA3IDYxLjYyNDcgMTAzLjk1NyA2MS4xNDg0IDEwMy42NTZDNjAuNjcyMiAxMDMuMzUyIDYwLjMwMTYgMTAyLjkyIDYwLjAzNjYgMTAyLjM2MkM1OS43NzUyIDEwMS44IDU5LjY0MSAxMDEuMTUgNTkuNjMzOCAxMDAuNDEyVjk5Ljg0ODFDNTkuNjMzOCA5OS4wOTYyIDU5Ljc2NDUgOTguNDMyIDYwLjAyNTkgOTcuODU1NUM2MC4yODczIDk3LjI3OSA2MC42NTYxIDk2LjgzODUgNjEuMTMyMyA5Ni41MzQyQzYxLjYxMjEgOTYuMjI2MiA2Mi4xNiA5Ni4wNzIzIDYyLjc3NTkgOTYuMDcyM0M2My40MDI1IDk2LjA3MjMgNjMuOTUzOSA5Ni4yMjQ0IDY0LjQzMDIgOTYuNTI4OEM2NC45MSA5Ni44Mjk2IDY1LjI3ODggOTcuMjY4MiA2NS41MzY2IDk3Ljg0NDdDNjUuNzk0NCA5OC40MTc2IDY1LjkyMzMgOTkuMDg1NCA2NS45MjMzIDk5Ljg0ODFWMTAwLjM0MlpNNjQuODk3NSA5OS44Mzc0QzY0Ljg5NzUgOTguOTEgNjQuNzExMyA5OC4xOTkyIDY0LjMzODkgOTcuNzA1MUM2My45NjY1IDk3LjIwNzQgNjMuNDQ1NSA5Ni45NTg1IDYyLjc3NTkgOTYuOTU4NUM2Mi4xMjQyIDk2Ljk1ODUgNjEuNjEwNCA5Ny4yMDc0IDYxLjIzNDQgOTcuNzA1MUM2MC44NjIgOTguMTk5MiA2MC42NzA0IDk4Ljg4NjcgNjAuNjU5NyA5OS43Njc2VjEwMC4zNDJDNjAuNjU5NyAxMDEuMjQxIDYwLjg0NzcgMTAxLjk0OCA2MS4yMjM2IDEwMi40NjRDNjEuNjAzMiAxMDIuOTc2IDYyLjEyNDIgMTAzLjIzMiA2Mi43ODY2IDEwMy4yMzJDNjMuNDUyNiAxMDMuMjMyIDYzLjk2ODMgMTAyLjk5IDY0LjMzMzUgMTAyLjUwN0M2NC42OTg3IDEwMi4wMiA2NC44ODY3IDEwMS4zMjMgNjQuODk3NSAxMDAuNDE3Vjk5LjgzNzRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik02OC4yNTQ0IDk4LjE4ODVMNjguMjg2NiA5OC45MTg5QzY4LjczMDYgOTguMzYwNCA2OS4zMTA3IDk4LjA4MTEgNzAuMDI2OSA5OC4wODExQzcxLjI1NSA5OC4wODExIDcxLjg3NDUgOTguNzczOSA3MS44ODUzIDEwMC4xNlYxMDRINzAuODkxNlYxMDAuMTU0QzcwLjg4OCA5OS43MzU0IDcwLjc5MTMgOTkuNDI1NiA3MC42MDE2IDk5LjIyNTFDNzAuNDE1NCA5OS4wMjQ2IDcwLjEyMzUgOTguOTI0MyA2OS43MjYxIDk4LjkyNDNDNjkuNDAzOCA5OC45MjQzIDY5LjEyMDkgOTkuMDEwMyA2OC44Nzc0IDk5LjE4MjFDNjguNjM0IDk5LjM1NCA2OC40NDQyIDk5LjU3OTYgNjguMzA4MSA5OS44NTg5VjEwNEg2Ny4zMTQ1Vjk4LjE4ODVINjguMjU0NFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTc1Ljc5NTQgMTA0LjEwN0M3NS4wMDc2IDEwNC4xMDcgNzQuMzY2NyAxMDMuODUgNzMuODcyNiAxMDMuMzM0QzczLjM3ODQgMTAyLjgxNSA3My4xMzEzIDEwMi4xMjIgNzMuMTMxMyAxMDEuMjU1VjEwMS4wNzNDNzMuMTMxMyAxMDAuNDk2IDczLjI0MDYgOTkuOTgyNCA3My40NTkgOTkuNTMxMkM3My42ODEgOTkuMDc2NSA3My45ODg5IDk4LjcyMiA3NC4zODI4IDk4LjQ2NzhDNzQuNzgwMyA5OC4yMSA3NS4yMSA5OC4wODExIDc1LjY3MTkgOTguMDgxMUM3Ni40Mjc0IDk4LjA4MTEgNzcuMDE0NiA5OC4zMjk5IDc3LjQzMzYgOTguODI3NkM3Ny44NTI1IDk5LjMyNTQgNzguMDYyIDEwMC4wMzggNzguMDYyIDEwMC45NjVWMTAxLjM3OUg3NC4xMjVDNzQuMTM5MyAxMDEuOTUyIDc0LjMwNTggMTAyLjQxNiA3NC42MjQ1IDEwMi43N0M3NC45NDY4IDEwMy4xMjEgNzUuMzU1IDEwMy4yOTYgNzUuODQ5MSAxMDMuMjk2Qzc2LjIgMTAzLjI5NiA3Ni40OTcyIDEwMy4yMjUgNzYuNzQwNyAxMDMuMDgyQzc2Ljk4NDIgMTAyLjkzOCA3Ny4xOTczIDEwMi43NDkgNzcuMzc5OSAxMDIuNTEyTDc3Ljk4NjggMTAyLjk4NUM3Ny40OTk4IDEwMy43MzMgNzYuNzY5NCAxMDQuMTA3IDc1Ljc5NTQgMTA0LjEwN1pNNzUuNjcxOSA5OC44OTc1Qzc1LjI3MDggOTguODk3NSA3NC45MzQyIDk5LjA0NDMgNzQuNjYyMSA5OS4zMzc5Qzc0LjM5IDk5LjYyNzkgNzQuMjIxNyAxMDAuMDM2IDc0LjE1NzIgMTAwLjU2Mkg3Ny4wNjg0VjEwMC40ODdDNzcuMDM5NyA5OS45ODI0IDc2LjkwMzYgOTkuNTkyMSA3Ni42NjAyIDk5LjMxNjRDNzYuNDE2NyA5OS4wMzcxIDc2LjA4NzIgOTguODk3NSA3NS42NzE5IDk4Ljg5NzVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik04MS4yODQ3IDEwMS4wODNINzguNjYzNlYxMDAuMjcySDgxLjI4NDdWMTAxLjA4M1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTg3LjQwMjMgMTAyLjYzTDg4LjUxOTUgOTguMTg4NUg4OS41MTMyTDg3LjgyMTMgMTA0SDg3LjAxNTZMODUuNjAzIDk5LjU5NTdMODQuMjI4IDEwNEg4My40MjI0TDgxLjczNTggOTguMTg4NUg4Mi43MjQxTDgzLjg2ODIgMTAyLjUzOUw4NS4yMjE3IDk4LjE4ODVIODYuMDIyTDg3LjQwMjMgMTAyLjYzWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNOTQuMTE2MiAxMDRDOTQuMDU4OSAxMDMuODg1IDk0LjAxMjQgMTAzLjY4MSA5My45NzY2IDEwMy4zODhDOTMuNTE0NiAxMDMuODY4IDkyLjk2MzIgMTA0LjEwNyA5Mi4zMjIzIDEwNC4xMDdDOTEuNzQ5MyAxMDQuMTA3IDkxLjI3ODUgMTAzLjk0NiA5MC45MDk3IDEwMy42MjRDOTAuNTQ0NCAxMDMuMjk4IDkwLjM2MTggMTAyLjg4NiA5MC4zNjE4IDEwMi4zODlDOTAuMzYxOCAxMDEuNzg0IDkwLjU5MSAxMDEuMzE0IDkxLjA0OTMgMTAwLjk4MUM5MS41MTEyIDEwMC42NDUgOTIuMTU5MyAxMDAuNDc3IDkyLjk5MzcgMTAwLjQ3N0g5My45NjA0VjEwMC4wMkM5My45NjA0IDk5LjY3MjcgOTMuODU2NiA5OS4zOTcgOTMuNjQ4OSA5OS4xOTI5QzkzLjQ0MTIgOTguOTg1MiA5My4xMzUxIDk4Ljg4MTMgOTIuNzMwNSA5OC44ODEzQzkyLjM3NiA5OC44ODEzIDkyLjA3ODggOTguOTcwOSA5MS44Mzg5IDk5LjE0OTlDOTEuNTk5IDk5LjMyODkgOTEuNDc5IDk5LjU0NTYgOTEuNDc5IDk5Ljc5OThIOTAuNDhDOTAuNDggOTkuNTA5OCA5MC41ODIgOTkuMjMwNSA5MC43ODYxIDk4Ljk2MTlDOTAuOTkzOCA5OC42ODk4IDkxLjI3MzEgOTguNDc0OSA5MS42MjQgOTguMzE3NEM5MS45Nzg1IDk4LjE1OTggOTIuMzY3IDk4LjA4MTEgOTIuNzg5NiA5OC4wODExQzkzLjQ1OTEgOTguMDgxMSA5My45ODM3IDk4LjI0OTMgOTQuMzYzMyA5OC41ODU5Qzk0Ljc0MjggOTguOTE4OSA5NC45Mzk4IDk5LjM3OTEgOTQuOTU0MSA5OS45NjYzVjEwMi42NDFDOTQuOTU0MSAxMDMuMTc1IDk1LjAyMjEgMTAzLjU5OSA5NS4xNTgyIDEwMy45MTRWMTA0SDk0LjExNjJaTTkyLjQ2NzMgMTAzLjI0M0M5Mi43Nzg4IDEwMy4yNDMgOTMuMDc0MiAxMDMuMTYyIDkzLjM1MzUgMTAzLjAwMUM5My42MzI4IDEwMi44NCA5My44MzUxIDEwMi42MyA5My45NjA0IDEwMi4zNzNWMTAxLjE4SDkzLjE4MTZDOTEuOTY0MiAxMDEuMTggOTEuMzU1NSAxMDEuNTM2IDkxLjM1NTUgMTAyLjI0OUM5MS4zNTU1IDEwMi41NjEgOTEuNDU5MyAxMDIuODA0IDkxLjY2NyAxMDIuOTc5QzkxLjg3NDcgMTAzLjE1NSA5Mi4xNDE0IDEwMy4yNDMgOTIuNDY3MyAxMDMuMjQzWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNOTguMzI3MSAxMDIuNTQ0TDk5LjY4MDcgOTguMTg4NUgxMDAuNzQ0TDk4LjQwNzcgMTA0Ljg5N0M5OC4wNDYxIDEwNS44NjQgOTcuNDcxNCAxMDYuMzQ3IDk2LjY4MzYgMTA2LjM0N0w5Ni40OTU2IDEwNi4zMzFMOTYuMTI1IDEwNi4yNjFWMTA1LjQ1Nkw5Ni4zOTM2IDEwNS40NzdDOTYuNzMwMSAxMDUuNDc3IDk2Ljk5MTUgMTA1LjQwOSA5Ny4xNzc3IDEwNS4yNzNDOTcuMzY3NSAxMDUuMTM3IDk3LjUyMzMgMTA0Ljg4OCA5Ny42NDUgMTA0LjUyNkw5Ny44NjUyIDEwMy45MzZMOTUuNzkyIDk4LjE4ODVIOTYuODc3TDk4LjMyNzEgMTAyLjU0NFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTY1LjkyMzMgMTQwLjM0MkM2NS45MjMzIDE0MS4xMDkgNjUuNzk0NCAxNDEuNzc4IDY1LjUzNjYgMTQyLjM1MUM2NS4yNzg4IDE0Mi45MiA2NC45MTM2IDE0My4zNTUgNjQuNDQwOSAxNDMuNjU2QzYzLjk2ODMgMTQzLjk1NyA2My40MTY4IDE0NC4xMDcgNjIuNzg2NiAxNDQuMTA3QzYyLjE3MDcgMTQ0LjEwNyA2MS42MjQ3IDE0My45NTcgNjEuMTQ4NCAxNDMuNjU2QzYwLjY3MjIgMTQzLjM1MiA2MC4zMDE2IDE0Mi45MiA2MC4wMzY2IDE0Mi4zNjJDNTkuNzc1MiAxNDEuOCA1OS42NDEgMTQxLjE1IDU5LjYzMzggMTQwLjQxMlYxMzkuODQ4QzU5LjYzMzggMTM5LjA5NiA1OS43NjQ1IDEzOC40MzIgNjAuMDI1OSAxMzcuODU1QzYwLjI4NzMgMTM3LjI3OSA2MC42NTYxIDEzNi44MzkgNjEuMTMyMyAxMzYuNTM0QzYxLjYxMjEgMTM2LjIyNiA2Mi4xNiAxMzYuMDcyIDYyLjc3NTkgMTM2LjA3MkM2My40MDI1IDEzNi4wNzIgNjMuOTUzOSAxMzYuMjI0IDY0LjQzMDIgMTM2LjUyOUM2NC45MSAxMzYuODMgNjUuMjc4OCAxMzcuMjY4IDY1LjUzNjYgMTM3Ljg0NUM2NS43OTQ0IDEzOC40MTggNjUuOTIzMyAxMzkuMDg1IDY1LjkyMzMgMTM5Ljg0OFYxNDAuMzQyWk02NC44OTc1IDEzOS44MzdDNjQuODk3NSAxMzguOTEgNjQuNzExMyAxMzguMTk5IDY0LjMzODkgMTM3LjcwNUM2My45NjY1IDEzNy4yMDcgNjMuNDQ1NSAxMzYuOTU4IDYyLjc3NTkgMTM2Ljk1OEM2Mi4xMjQyIDEzNi45NTggNjEuNjEwNCAxMzcuMjA3IDYxLjIzNDQgMTM3LjcwNUM2MC44NjIgMTM4LjE5OSA2MC42NzA0IDEzOC44ODcgNjAuNjU5NyAxMzkuNzY4VjE0MC4zNDJDNjAuNjU5NyAxNDEuMjQxIDYwLjg0NzcgMTQxLjk0OCA2MS4yMjM2IDE0Mi40NjRDNjEuNjAzMiAxNDIuOTc2IDYyLjEyNDIgMTQzLjIzMiA2Mi43ODY2IDE0My4yMzJDNjMuNDUyNiAxNDMuMjMyIDYzLjk2ODMgMTQyLjk5IDY0LjMzMzUgMTQyLjUwN0M2NC42OTg3IDE0Mi4wMiA2NC44ODY3IDE0MS4zMjMgNjQuODk3NSAxNDAuNDE3VjEzOS44MzdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik02OC4yNTQ0IDEzOC4xODhMNjguMjg2NiAxMzguOTE5QzY4LjczMDYgMTM4LjM2IDY5LjMxMDcgMTM4LjA4MSA3MC4wMjY5IDEzOC4wODFDNzEuMjU1IDEzOC4wODEgNzEuODc0NSAxMzguNzc0IDcxLjg4NTMgMTQwLjE2VjE0NEg3MC44OTE2VjE0MC4xNTRDNzAuODg4IDEzOS43MzUgNzAuNzkxMyAxMzkuNDI2IDcwLjYwMTYgMTM5LjIyNUM3MC40MTU0IDEzOS4wMjUgNzAuMTIzNSAxMzguOTI0IDY5LjcyNjEgMTM4LjkyNEM2OS40MDM4IDEzOC45MjQgNjkuMTIwOSAxMzkuMDEgNjguODc3NCAxMzkuMTgyQzY4LjYzNCAxMzkuMzU0IDY4LjQ0NDIgMTM5LjU4IDY4LjMwODEgMTM5Ljg1OVYxNDRINjcuMzE0NVYxMzguMTg4SDY4LjI1NDRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik03NS43OTU0IDE0NC4xMDdDNzUuMDA3NiAxNDQuMTA3IDc0LjM2NjcgMTQzLjg1IDczLjg3MjYgMTQzLjMzNEM3My4zNzg0IDE0Mi44MTUgNzMuMTMxMyAxNDIuMTIyIDczLjEzMTMgMTQxLjI1NVYxNDEuMDczQzczLjEzMTMgMTQwLjQ5NiA3My4yNDA2IDEzOS45ODIgNzMuNDU5IDEzOS41MzFDNzMuNjgxIDEzOS4wNzYgNzMuOTg4OSAxMzguNzIyIDc0LjM4MjggMTM4LjQ2OEM3NC43ODAzIDEzOC4yMSA3NS4yMSAxMzguMDgxIDc1LjY3MTkgMTM4LjA4MUM3Ni40Mjc0IDEzOC4wODEgNzcuMDE0NiAxMzguMzMgNzcuNDMzNiAxMzguODI4Qzc3Ljg1MjUgMTM5LjMyNSA3OC4wNjIgMTQwLjAzOCA3OC4wNjIgMTQwLjk2NVYxNDEuMzc5SDc0LjEyNUM3NC4xMzkzIDE0MS45NTIgNzQuMzA1OCAxNDIuNDE2IDc0LjYyNDUgMTQyLjc3Qzc0Ljk0NjggMTQzLjEyMSA3NS4zNTUgMTQzLjI5NiA3NS44NDkxIDE0My4yOTZDNzYuMiAxNDMuMjk2IDc2LjQ5NzIgMTQzLjIyNSA3Ni43NDA3IDE0My4wODJDNzYuOTg0MiAxNDIuOTM4IDc3LjE5NzMgMTQyLjc0OSA3Ny4zNzk5IDE0Mi41MTJMNzcuOTg2OCAxNDIuOTg1Qzc3LjQ5OTggMTQzLjczMyA3Ni43Njk0IDE0NC4xMDcgNzUuNzk1NCAxNDQuMTA3Wk03NS42NzE5IDEzOC44OTdDNzUuMjcwOCAxMzguODk3IDc0LjkzNDIgMTM5LjA0NCA3NC42NjIxIDEzOS4zMzhDNzQuMzkgMTM5LjYyOCA3NC4yMjE3IDE0MC4wMzYgNzQuMTU3MiAxNDAuNTYySDc3LjA2ODRWMTQwLjQ4N0M3Ny4wMzk3IDEzOS45ODIgNzYuOTAzNiAxMzkuNTkyIDc2LjY2MDIgMTM5LjMxNkM3Ni40MTY3IDEzOS4wMzcgNzYuMDg3MiAxMzguODk3IDc1LjY3MTkgMTM4Ljg5N1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTgxLjI4NDcgMTQxLjA4M0g3OC42NjM2VjE0MC4yNzJIODEuMjg0N1YxNDEuMDgzWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNODcuNDAyMyAxNDIuNjNMODguNTE5NSAxMzguMTg4SDg5LjUxMzJMODcuODIxMyAxNDRIODcuMDE1Nkw4NS42MDMgMTM5LjU5Nkw4NC4yMjggMTQ0SDgzLjQyMjRMODEuNzM1OCAxMzguMTg4SDgyLjcyNDFMODMuODY4MiAxNDIuNTM5TDg1LjIyMTcgMTM4LjE4OEg4Ni4wMjJMODcuNDAyMyAxNDIuNjNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik05NC4xMTYyIDE0NEM5NC4wNTg5IDE0My44ODUgOTQuMDEyNCAxNDMuNjgxIDkzLjk3NjYgMTQzLjM4OEM5My41MTQ2IDE0My44NjggOTIuOTYzMiAxNDQuMTA3IDkyLjMyMjMgMTQ0LjEwN0M5MS43NDkzIDE0NC4xMDcgOTEuMjc4NSAxNDMuOTQ2IDkwLjkwOTcgMTQzLjYyNEM5MC41NDQ0IDE0My4yOTggOTAuMzYxOCAxNDIuODg2IDkwLjM2MTggMTQyLjM4OUM5MC4zNjE4IDE0MS43ODQgOTAuNTkxIDE0MS4zMTQgOTEuMDQ5MyAxNDAuOTgxQzkxLjUxMTIgMTQwLjY0NSA5Mi4xNTkzIDE0MC40NzcgOTIuOTkzNyAxNDAuNDc3SDkzLjk2MDRWMTQwLjAyQzkzLjk2MDQgMTM5LjY3MyA5My44NTY2IDEzOS4zOTcgOTMuNjQ4OSAxMzkuMTkzQzkzLjQ0MTIgMTM4Ljk4NSA5My4xMzUxIDEzOC44ODEgOTIuNzMwNSAxMzguODgxQzkyLjM3NiAxMzguODgxIDkyLjA3ODggMTM4Ljk3MSA5MS44Mzg5IDEzOS4xNUM5MS41OTkgMTM5LjMyOSA5MS40NzkgMTM5LjU0NiA5MS40NzkgMTM5LjhIOTAuNDhDOTAuNDggMTM5LjUxIDkwLjU4MiAxMzkuMjMgOTAuNzg2MSAxMzguOTYyQzkwLjk5MzggMTM4LjY5IDkxLjI3MzEgMTM4LjQ3NSA5MS42MjQgMTM4LjMxN0M5MS45Nzg1IDEzOC4xNiA5Mi4zNjcgMTM4LjA4MSA5Mi43ODk2IDEzOC4wODFDOTMuNDU5MSAxMzguMDgxIDkzLjk4MzcgMTM4LjI0OSA5NC4zNjMzIDEzOC41ODZDOTQuNzQyOCAxMzguOTE5IDk0LjkzOTggMTM5LjM3OSA5NC45NTQxIDEzOS45NjZWMTQyLjY0MUM5NC45NTQxIDE0My4xNzUgOTUuMDIyMSAxNDMuNTk5IDk1LjE1ODIgMTQzLjkxNFYxNDRIOTQuMTE2MlpNOTIuNDY3MyAxNDMuMjQzQzkyLjc3ODggMTQzLjI0MyA5My4wNzQyIDE0My4xNjIgOTMuMzUzNSAxNDMuMDAxQzkzLjYzMjggMTQyLjg0IDkzLjgzNTEgMTQyLjYzIDkzLjk2MDQgMTQyLjM3M1YxNDEuMThIOTMuMTgxNkM5MS45NjQyIDE0MS4xOCA5MS4zNTU1IDE0MS41MzYgOTEuMzU1NSAxNDIuMjQ5QzkxLjM1NTUgMTQyLjU2MSA5MS40NTkzIDE0Mi44MDQgOTEuNjY3IDE0Mi45NzlDOTEuODc0NyAxNDMuMTU1IDkyLjE0MTQgMTQzLjI0MyA5Mi40NjczIDE0My4yNDNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik05OC4zMjcxIDE0Mi41NDRMOTkuNjgwNyAxMzguMTg4SDEwMC43NDRMOTguNDA3NyAxNDQuODk3Qzk4LjA0NjEgMTQ1Ljg2NCA5Ny40NzE0IDE0Ni4zNDcgOTYuNjgzNiAxNDYuMzQ3TDk2LjQ5NTYgMTQ2LjMzMUw5Ni4xMjUgMTQ2LjI2MVYxNDUuNDU2TDk2LjM5MzYgMTQ1LjQ3N0M5Ni43MzAxIDE0NS40NzcgOTYuOTkxNSAxNDUuNDA5IDk3LjE3NzcgMTQ1LjI3M0M5Ny4zNjc1IDE0NS4xMzcgOTcuNTIzMyAxNDQuODg4IDk3LjY0NSAxNDQuNTI2TDk3Ljg2NTIgMTQzLjkzNkw5NS43OTIgMTM4LjE4OEg5Ni44NzdMOTguMzI3MSAxNDIuNTQ0WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTIuNzczNCAxMDMuNDc5QzEyLjc3MzQgMTAzLjMwNyAxMi44MjM2IDEwMy4xNjQgMTIuOTIzOCAxMDMuMDQ5QzEzLjAyNzcgMTAyLjkzNSAxMy4xODE2IDEwMi44NzcgMTMuMzg1NyAxMDIuODc3QzEzLjU4OTggMTAyLjg3NyAxMy43NDM4IDEwMi45MzUgMTMuODQ3NyAxMDMuMDQ5QzEzLjk1NTEgMTAzLjE2NCAxNC4wMDg4IDEwMy4zMDcgMTQuMDA4OCAxMDMuNDc5QzE0LjAwODggMTAzLjY0NCAxMy45NTUxIDEwMy43ODIgMTMuODQ3NyAxMDMuODkzQzEzLjc0MzggMTA0LjAwNCAxMy41ODk4IDEwNC4wNTkgMTMuMzg1NyAxMDQuMDU5QzEzLjE4MTYgMTA0LjA1OSAxMy4wMjc3IDEwNC4wMDQgMTIuOTIzOCAxMDMuODkzQzEyLjgyMzYgMTAzLjc4MiAxMi43NzM0IDEwMy42NDQgMTIuNzczNCAxMDMuNDc5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTUuNjczOCAxMDMuNDc5QzE1LjY3MzggMTAzLjMwNyAxNS43MjQgMTAzLjE2NCAxNS44MjQyIDEwMy4wNDlDMTUuOTI4MSAxMDIuOTM1IDE2LjA4MiAxMDIuODc3IDE2LjI4NjEgMTAyLjg3N0MxNi40OTAyIDEwMi44NzcgMTYuNjQ0MiAxMDIuOTM1IDE2Ljc0OCAxMDMuMDQ5QzE2Ljg1NTUgMTAzLjE2NCAxNi45MDkyIDEwMy4zMDcgMTYuOTA5MiAxMDMuNDc5QzE2LjkwOTIgMTAzLjY0NCAxNi44NTU1IDEwMy43ODIgMTYuNzQ4IDEwMy44OTNDMTYuNjQ0MiAxMDQuMDA0IDE2LjQ5MDIgMTA0LjA1OSAxNi4yODYxIDEwNC4wNTlDMTYuMDgyIDEwNC4wNTkgMTUuOTI4MSAxMDQuMDA0IDE1LjgyNDIgMTAzLjg5M0MxNS43MjQgMTAzLjc4MiAxNS42NzM4IDEwMy42NDQgMTUuNjczOCAxMDMuNDc5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTguMjg5NiAxMDEuMDQxQzE4LjI4OTYgMTAwLjQ3MSAxOC40MDA2IDk5Ljk1OTEgMTguNjIyNiA5OS41MDQ0QzE4Ljg0ODEgOTkuMDQ5NiAxOS4xNTk3IDk4LjY5ODcgMTkuNTU3MSA5OC40NTE3QzE5Ljk1ODIgOTguMjA0NiAyMC40MTQ3IDk4LjA4MTEgMjAuOTI2OCA5OC4wODExQzIxLjcxODEgOTguMDgxMSAyMi4zNTczIDk4LjM1NSAyMi44NDQyIDk4LjkwMjhDMjMuMzM0OCA5OS40NTA3IDIzLjU4MDEgMTAwLjE3OSAyMy41ODAxIDEwMS4wODlWMTAxLjE1OUMyMy41ODAxIDEwMS43MjQgMjMuNDcwOSAxMDIuMjMzIDIzLjI1MjQgMTAyLjY4NEMyMy4wMzc2IDEwMy4xMzIgMjIuNzI3OSAxMDMuNDgxIDIyLjMyMzIgMTAzLjczMUMyMS45MjIyIDEwMy45ODIgMjEuNDYwMyAxMDQuMTA3IDIwLjkzNzUgMTA0LjEwN0MyMC4xNDk3IDEwNC4xMDcgMTkuNTEwNiAxMDMuODMzIDE5LjAyIDEwMy4yODZDMTguNTMzIDEwMi43MzggMTguMjg5NiAxMDIuMDEzIDE4LjI4OTYgMTAxLjExVjEwMS4wNDFaTTE5LjI4ODYgMTAxLjE1OUMxOS4yODg2IDEwMS44MDMgMTkuNDM3MiAxMDIuMzIxIDE5LjczNDQgMTAyLjcxMUMyMC4wMzUyIDEwMy4xMDEgMjAuNDM2MiAxMDMuMjk2IDIwLjkzNzUgMTAzLjI5NkMyMS40NDI0IDEwMy4yOTYgMjEuODQzNCAxMDMuMDk5IDIyLjE0MDYgMTAyLjcwNkMyMi40Mzc4IDEwMi4zMDggMjIuNTg2NCAxMDEuNzUzIDIyLjU4NjQgMTAxLjA0MUMyMi41ODY0IDEwMC40MDMgMjIuNDM0MiA5OS44ODc1IDIyLjEyOTkgOTkuNDkzN0MyMS44MjkxIDk5LjA5NjIgMjEuNDI4MSA5OC44OTc1IDIwLjkyNjggOTguODk3NUMyMC40MzYyIDk4Ljg5NzUgMjAuMDQwNSA5OS4wOTI2IDE5LjczOTcgOTkuNDgyOUMxOS40MzkgOTkuODczMiAxOS4yODg2IDEwMC40MzIgMTkuMjg4NiAxMDEuMTU5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMjYuMTY4OSA5OS42MDY0SDI2LjkxNTVDMjcuMzg0NiA5OS41OTkzIDI3Ljc1MzQgOTkuNDc1NyAyOC4wMjIgOTkuMjM1OEMyOC4yOTA1IDk4Ljk5NTkgMjguNDI0OCA5OC42NzE5IDI4LjQyNDggOTguMjYzN0MyOC40MjQ4IDk3LjM0NyAyNy45NjgzIDk2Ljg4ODcgMjcuMDU1MiA5Ni44ODg3QzI2LjYyNTUgOTYuODg4NyAyNi4yODE3IDk3LjAxMjIgMjYuMDIzOSA5Ny4yNTkzQzI1Ljc2OTcgOTcuNTAyOCAyNS42NDI2IDk3LjgyNjggMjUuNjQyNiA5OC4yMzE0SDI0LjY0ODlDMjQuNjQ4OSA5Ny42MTIgMjQuODc0NSA5Ny4wOTgxIDI1LjMyNTcgOTYuNjg5OUMyNS43ODA0IDk2LjI3ODIgMjYuMzU2OSA5Ni4wNzIzIDI3LjA1NTIgOTYuMDcyM0MyNy43OTI4IDk2LjA3MjMgMjguMzcxMSA5Ni4yNjc0IDI4Ljc5IDk2LjY1NzdDMjkuMjA5IDk3LjA0OCAyOS40MTg1IDk3LjU5MDUgMjkuNDE4NSA5OC4yODUyQzI5LjQxODUgOTguNjI1MyAyOS4zMDc1IDk4Ljk1NDggMjkuMDg1NCA5OS4yNzM0QzI4Ljg2NyA5OS41OTIxIDI4LjU2OCA5OS44MzAyIDI4LjE4ODUgOTkuOTg3OEMyOC42MTgyIDEwMC4xMjQgMjguOTQ5NCAxMDAuMzQ5IDI5LjE4MjEgMTAwLjY2NUMyOS40MTg1IDEwMC45OCAyOS41MzY2IDEwMS4zNjUgMjkuNTM2NiAxMDEuODE5QzI5LjUzNjYgMTAyLjUyMSAyOS4zMDc1IDEwMy4wNzggMjguODQ5MSAxMDMuNDlDMjguMzkwOCAxMDMuOTAyIDI3Ljc5NDYgMTA0LjEwNyAyNy4wNjA1IDEwNC4xMDdDMjYuMzI2NSAxMDQuMTA3IDI1LjcyODUgMTAzLjkwOSAyNS4yNjY2IDEwMy41MTFDMjQuODA4MyAxMDMuMTE0IDI0LjU3OTEgMTAyLjU4OSAyNC41NzkxIDEwMS45MzhIMjUuNTc4MUMyNS41NzgxIDEwMi4zNDkgMjUuNzEyNCAxMDIuNjc5IDI1Ljk4MSAxMDIuOTI2QzI2LjI0OTUgMTAzLjE3MyAyNi42MDk0IDEwMy4yOTYgMjcuMDYwNSAxMDMuMjk2QzI3LjU0MDQgMTAzLjI5NiAyNy45MDc0IDEwMy4xNzEgMjguMTYxNiAxMDIuOTJDMjguNDE1OSAxMDIuNjcgMjguNTQzIDEwMi4zMSAyOC41NDMgMTAxLjg0MUMyOC41NDMgMTAxLjM4NiAyOC40MDMzIDEwMS4wMzcgMjguMTI0IDEwMC43OTNDMjcuODQ0NyAxMDAuNTUgMjcuNDQxOSAxMDAuNDI1IDI2LjkxNTUgMTAwLjQxN0gyNi4xNjg5Vjk5LjYwNjRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0zMC43NjEyIDEwMS4wNDZDMzAuNzYxMiAxMDAuMTU0IDMwLjk3MjUgOTkuNDM4MiAzMS4zOTUgOTguODk3NUMzMS44MTc1IDk4LjM1MzIgMzIuMzcwOCA5OC4wODExIDMzLjA1NDcgOTguMDgxMUMzMy43MzUgOTguMDgxMSAzNC4yNzM5IDk4LjMxMzggMzQuNjcxNCA5OC43NzkzVjk1Ljc1SDM1LjY2NVYxMDRIMzQuNzUyTDM0LjcwMzYgMTAzLjM3N0MzNC4zMDYyIDEwMy44NjQgMzMuNzUyOSAxMDQuMTA3IDMzLjA0MzkgMTA0LjEwN0MzMi4zNzA4IDEwNC4xMDcgMzEuODIxMSAxMDMuODMyIDMxLjM5NSAxMDMuMjhDMzAuOTcyNSAxMDIuNzI5IDMwLjc2MTIgMTAyLjAwOSAzMC43NjEyIDEwMS4xMjFWMTAxLjA0NlpNMzEuNzU0OSAxMDEuMTU5QzMxLjc1NDkgMTAxLjgxOCAzMS44OTEgMTAyLjMzMyAzMi4xNjMxIDEwMi43MDZDMzIuNDM1MiAxMDMuMDc4IDMyLjgxMTIgMTAzLjI2NCAzMy4yOTEgMTAzLjI2NEMzMy45MjEyIDEwMy4yNjQgMzQuMzgxMyAxMDIuOTgxIDM0LjY3MTQgMTAyLjQxNlY5OS43NDYxQzM0LjM3NDIgOTkuMTk4MiAzMy45MTc2IDk4LjkyNDMgMzMuMzAxOCA5OC45MjQzQzMyLjgxNDggOTguOTI0MyAzMi40MzUyIDk5LjExMjMgMzIuMTYzMSA5OS40ODgzQzMxLjg5MSA5OS44NjQzIDMxLjc1NDkgMTAwLjQyMSAzMS43NTQ5IDEwMS4xNTlaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik00MC4wMjEgOTkuMDgwMUMzOS44NzA2IDk5LjA1NSAzOS43MDc3IDk5LjA0MjUgMzkuNTMyMiA5OS4wNDI1QzM4Ljg4MDUgOTkuMDQyNSAzOC40MzgzIDk5LjMyIDM4LjIwNTYgOTkuODc1VjEwNEgzNy4yMTE5Vjk4LjE4ODVIMzguMTc4N0wzOC4xOTQ4IDk4Ljg1OTlDMzguNTIwNyA5OC4zNDA3IDM4Ljk4MjYgOTguMDgxMSAzOS41ODA2IDk4LjA4MTFDMzkuNzczOSA5OC4wODExIDM5LjkyMDcgOTguMTA2MSA0MC4wMjEgOTguMTU2MlY5OS4wODAxWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNDEuODc5NCA5OC4xODg1TDQxLjkxMTYgOTguOTE4OUM0Mi4zNTU2IDk4LjM2MDQgNDIuOTM1NyA5OC4wODExIDQzLjY1MTkgOTguMDgxMUM0NC44OCA5OC4wODExIDQ1LjQ5OTUgOTguNzczOSA0NS41MTAzIDEwMC4xNlYxMDRINDQuNTE2NlYxMDAuMTU0QzQ0LjUxMyA5OS43MzU0IDQ0LjQxNjMgOTkuNDI1NiA0NC4yMjY2IDk5LjIyNTFDNDQuMDQwNCA5OS4wMjQ2IDQzLjc0ODUgOTguOTI0MyA0My4zNTExIDk4LjkyNDNDNDMuMDI4OCA5OC45MjQzIDQyLjc0NTkgOTkuMDEwMyA0Mi41MDI0IDk5LjE4MjFDNDIuMjU5IDk5LjM1NCA0Mi4wNjkyIDk5LjU3OTYgNDEuOTMzMSA5OS44NTg5VjEwNEg0MC45Mzk1Vjk4LjE4ODVINDEuODc5NFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTEyLjc3MzQgMTQzLjQ3OUMxMi43NzM0IDE0My4zMDcgMTIuODIzNiAxNDMuMTY0IDEyLjkyMzggMTQzLjA0OUMxMy4wMjc3IDE0Mi45MzUgMTMuMTgxNiAxNDIuODc3IDEzLjM4NTcgMTQyLjg3N0MxMy41ODk4IDE0Mi44NzcgMTMuNzQzOCAxNDIuOTM1IDEzLjg0NzcgMTQzLjA0OUMxMy45NTUxIDE0My4xNjQgMTQuMDA4OCAxNDMuMzA3IDE0LjAwODggMTQzLjQ3OUMxNC4wMDg4IDE0My42NDQgMTMuOTU1MSAxNDMuNzgyIDEzLjg0NzcgMTQzLjg5M0MxMy43NDM4IDE0NC4wMDQgMTMuNTg5OCAxNDQuMDU5IDEzLjM4NTcgMTQ0LjA1OUMxMy4xODE2IDE0NC4wNTkgMTMuMDI3NyAxNDQuMDA0IDEyLjkyMzggMTQzLjg5M0MxMi44MjM2IDE0My43ODIgMTIuNzczNCAxNDMuNjQ0IDEyLjc3MzQgMTQzLjQ3OVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTE1LjY3MzggMTQzLjQ3OUMxNS42NzM4IDE0My4zMDcgMTUuNzI0IDE0My4xNjQgMTUuODI0MiAxNDMuMDQ5QzE1LjkyODEgMTQyLjkzNSAxNi4wODIgMTQyLjg3NyAxNi4yODYxIDE0Mi44NzdDMTYuNDkwMiAxNDIuODc3IDE2LjY0NDIgMTQyLjkzNSAxNi43NDggMTQzLjA0OUMxNi44NTU1IDE0My4xNjQgMTYuOTA5MiAxNDMuMzA3IDE2LjkwOTIgMTQzLjQ3OUMxNi45MDkyIDE0My42NDQgMTYuODU1NSAxNDMuNzgyIDE2Ljc0OCAxNDMuODkzQzE2LjY0NDIgMTQ0LjAwNCAxNi40OTAyIDE0NC4wNTkgMTYuMjg2MSAxNDQuMDU5QzE2LjA4MiAxNDQuMDU5IDE1LjkyODEgMTQ0LjAwNCAxNS44MjQyIDE0My44OTNDMTUuNzI0IDE0My43ODIgMTUuNjczOCAxNDMuNjQ0IDE1LjY3MzggMTQzLjQ3OVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTIxLjM2MTggMTM5LjA4QzIxLjIxMTQgMTM5LjA1NSAyMS4wNDg1IDEzOS4wNDIgMjAuODczIDEzOS4wNDJDMjAuMjIxNCAxMzkuMDQyIDE5Ljc3OTEgMTM5LjMyIDE5LjU0NjQgMTM5Ljg3NVYxNDRIMTguNTUyN1YxMzguMTg4SDE5LjUxOTVMMTkuNTM1NiAxMzguODZDMTkuODYxNSAxMzguMzQxIDIwLjMyMzQgMTM4LjA4MSAyMC45MjE0IDEzOC4wODFDMjEuMTE0NyAxMzguMDgxIDIxLjI2MTYgMTM4LjEwNiAyMS4zNjE4IDEzOC4xNTZWMTM5LjA4WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMjQuMjk0NCAxNDIuNjUyTDI1LjczMzkgMTM4LjE4OEgyNi43NDlMMjQuNjY1IDE0NEgyMy45MDc3TDIxLjgwMjIgMTM4LjE4OEgyMi44MTc0TDI0LjI5NDQgMTQyLjY1MloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTMxLjQxMTEgMTQwLjU2MkMzMS4yMDM1IDE0MC44MSAzMC45NTQ2IDE0MS4wMDggMzAuNjY0NiAxNDEuMTU5QzMwLjM3ODEgMTQxLjMwOSAzMC4wNjMgMTQxLjM4NCAyOS43MTkyIDE0MS4zODRDMjkuMjY4MSAxNDEuMzg0IDI4Ljg3NDIgMTQxLjI3MyAyOC41Mzc2IDE0MS4wNTFDMjguMjA0NiAxNDAuODI5IDI3Ljk0NjggMTQwLjUxOCAyNy43NjQyIDE0MC4xMTdDMjcuNTgxNSAxMzkuNzEyIDI3LjQ5MDIgMTM5LjI2NiAyNy40OTAyIDEzOC43NzlDMjcuNDkwMiAxMzguMjU3IDI3LjU4ODcgMTM3Ljc4NiAyNy43ODU2IDEzNy4zNjdDMjcuOTg2MiAxMzYuOTQ4IDI4LjI2OSAxMzYuNjI3IDI4LjYzNDMgMTM2LjQwNUMyOC45OTk1IDEzNi4xODMgMjkuNDI1NiAxMzYuMDcyIDI5LjkxMjYgMTM2LjA3MkMzMC42ODYgMTM2LjA3MiAzMS4yOTQ4IDEzNi4zNjIgMzEuNzM4OCAxMzYuOTQyQzMyLjE4NjQgMTM3LjUxOSAzMi40MTAyIDEzOC4zMDcgMzIuNDEwMiAxMzkuMzA2VjEzOS41OTZDMzIuNDEwMiAxNDEuMTE4IDMyLjEwOTQgMTQyLjIyOSAzMS41MDc4IDE0Mi45MzFDMzAuOTA2MiAxNDMuNjI5IDI5Ljk5ODUgMTQzLjk4NyAyOC43ODQ3IDE0NC4wMDVIMjguNTkxM1YxNDMuMTY3SDI4LjgwMDhDMjkuNjIwOCAxNDMuMTUzIDMwLjI1MSAxNDIuOTQgMzAuNjkxNCAxNDIuNTI4QzMxLjEzMTggMTQyLjExMyAzMS4zNzE3IDE0MS40NTggMzEuNDExMSAxNDAuNTYyWk0yOS44ODA0IDE0MC41NjJDMzAuMjEzNCAxNDAuNTYyIDMwLjUxOTUgMTQwLjQ2IDMwLjc5ODggMTQwLjI1NkMzMS4wODE3IDE0MC4wNTIgMzEuMjg3NiAxMzkuOCAzMS40MTY1IDEzOS40OTlWMTM5LjEwMkMzMS40MTY1IDEzOC40NSAzMS4yNzUxIDEzNy45MiAzMC45OTIyIDEzNy41MTJDMzAuNzA5MyAxMzcuMTA0IDMwLjM1MTIgMTM2Ljg5OSAyOS45MTggMTM2Ljg5OUMyOS40ODExIDEzNi44OTkgMjkuMTMwMiAxMzcuMDY4IDI4Ljg2NTIgMTM3LjQwNEMyOC42MDAzIDEzNy43MzcgMjguNDY3OCAxMzguMTc4IDI4LjQ2NzggMTM4LjcyNkMyOC40Njc4IDEzOS4yNTkgMjguNTk0OSAxMzkuNyAyOC44NDkxIDE0MC4wNDdDMjkuMTA2OSAxNDAuMzkxIDI5LjQ1MDcgMTQwLjU2MiAyOS44ODA0IDE0MC41NjJaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0zNS41MDM5IDE0MS4zMDlMMzQuODgwOSAxNDEuOTU5VjE0NEgzMy44ODcyVjEzNS43NUgzNC44ODA5VjE0MC43NEwzNS40MTI2IDE0MC4xMDFMMzcuMjIyNyAxMzguMTg4SDM4LjQzMTJMMzYuMTY5OSAxNDAuNjE2TDM4LjY5NDMgMTQ0SDM3LjUyODhMMzUuNTAzOSAxNDEuMzA5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNDEuNzYxMiAxNDQuMTA3QzQwLjk3MzUgMTQ0LjEwNyA0MC4zMzI1IDE0My44NSAzOS44Mzg0IDE0My4zMzRDMzkuMzQ0MiAxNDIuODE1IDM5LjA5NzIgMTQyLjEyMiAzOS4wOTcyIDE0MS4yNTVWMTQxLjA3M0MzOS4wOTcyIDE0MC40OTYgMzkuMjA2NCAxMzkuOTgyIDM5LjQyNDggMTM5LjUzMUMzOS42NDY4IDEzOS4wNzYgMzkuOTU0OCAxMzguNzIyIDQwLjM0ODYgMTM4LjQ2OEM0MC43NDYxIDEzOC4yMSA0MS4xNzU4IDEzOC4wODEgNDEuNjM3NyAxMzguMDgxQzQyLjM5MzIgMTM4LjA4MSA0Mi45ODA1IDEzOC4zMyA0My4zOTk0IDEzOC44MjhDNDMuODE4NCAxMzkuMzI1IDQ0LjAyNzggMTQwLjAzOCA0NC4wMjc4IDE0MC45NjVWMTQxLjM3OUg0MC4wOTA4QzQwLjEwNTEgMTQxLjk1MiA0MC4yNzE2IDE0Mi40MTYgNDAuNTkwMyAxNDIuNzdDNDAuOTEyNiAxNDMuMTIxIDQxLjMyMDggMTQzLjI5NiA0MS44MTQ5IDE0My4yOTZDNDIuMTY1OSAxNDMuMjk2IDQyLjQ2MzEgMTQzLjIyNSA0Mi43MDY1IDE0My4wODJDNDIuOTUgMTQyLjkzOCA0My4xNjMxIDE0Mi43NDkgNDMuMzQ1NyAxNDIuNTEyTDQzLjk1MjYgMTQyLjk4NUM0My40NjU3IDE0My43MzMgNDIuNzM1MiAxNDQuMTA3IDQxLjc2MTIgMTQ0LjEwN1pNNDEuNjM3NyAxMzguODk3QzQxLjIzNjcgMTM4Ljg5NyA0MC45MDAxIDEzOS4wNDQgNDAuNjI3OSAxMzkuMzM4QzQwLjM1NTggMTM5LjYyOCA0MC4xODc1IDE0MC4wMzYgNDAuMTIzIDE0MC41NjJINDMuMDM0MlYxNDAuNDg3QzQzLjAwNTUgMTM5Ljk4MiA0Mi44Njk1IDEzOS41OTIgNDIuNjI2IDEzOS4zMTZDNDIuMzgyNSAxMzkuMDM3IDQyLjA1MzEgMTM4Ljg5NyA0MS42Mzc3IDEzOC44OTdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xNDYuNDQ1IDU3LjI3NTRIMTQ0LjAwN1Y2NEgxNDIuNjU5VjU3LjI3NTRIMTQwLjI0MlY1Ni4xNzk3SDE0Ni40NDVWNTcuMjc1NFoiIGZpbGw9IiNGRkE1MDAiLz4KPHBhdGggZD0iTTE0OC43MzkgNjRIMTQ3LjQzNFY1OC4xODg1SDE0OC43MzlWNjRaTTE0Ny4zNTMgNTYuNjc5MkMxNDcuMzUzIDU2LjQ3ODcgMTQ3LjQxNiA1Ni4zMTIyIDE0Ny41NDEgNTYuMTc5N0MxNDcuNjcgNTYuMDQ3MiAxNDcuODUzIDU1Ljk4MSAxNDguMDg5IDU1Ljk4MUMxNDguMzI1IDU1Ljk4MSAxNDguNTA4IDU2LjA0NzIgMTQ4LjYzNyA1Ni4xNzk3QzE0OC43NjYgNTYuMzEyMiAxNDguODMgNTYuNDc4NyAxNDguODMgNTYuNjc5MkMxNDguODMgNTYuODc2MSAxNDguNzY2IDU3LjA0MDkgMTQ4LjYzNyA1Ny4xNzMzQzE0OC41MDggNTcuMzAyMiAxNDguMzI1IDU3LjM2NjcgMTQ4LjA4OSA1Ny4zNjY3QzE0Ny44NTMgNTcuMzY2NyAxNDcuNjcgNTcuMzAyMiAxNDcuNTQxIDU3LjE3MzNDMTQ3LjQxNiA1Ny4wNDA5IDE0Ny4zNTMgNTYuODc2MSAxNDcuMzUzIDU2LjY3OTJaIiBmaWxsPSIjRkZBNTAwIi8+CjxwYXRoIGQ9Ik0xNTEuMzkyIDU4LjE4ODVMMTUxLjQzIDU4Ljc5NTRDMTUxLjgzOCA1OC4zMTkyIDE1Mi4zOTYgNTguMDgxMSAxNTMuMTA1IDU4LjA4MTFDMTUzLjg4MiA1OC4wODExIDE1NC40MTQgNTguMzc4MyAxNTQuNzAxIDU4Ljk3MjdDMTU1LjEyMyA1OC4zNzgzIDE1NS43MTggNTguMDgxMSAxNTYuNDg0IDU4LjA4MTFDMTU3LjEyNSA1OC4wODExIDE1Ny42MDEgNTguMjU4MyAxNTcuOTEzIDU4LjYxMjhDMTU4LjIyOCA1OC45NjczIDE1OC4zODkgNTkuNDkwMSAxNTguMzk2IDYwLjE4MTJWNjRIMTU3LjA5MVY2MC4yMTg4QzE1Ny4wOTEgNTkuODQ5OSAxNTcuMDEgNTkuNTc5NiAxNTYuODQ5IDU5LjQwNzdDMTU2LjY4OCA1OS4yMzU4IDE1Ni40MjEgNTkuMTQ5OSAxNTYuMDQ5IDU5LjE0OTlDMTU1Ljc1MiA1OS4xNDk5IDE1NS41MDggNTkuMjMwNSAxNTUuMzE4IDU5LjM5MTZDMTU1LjEzMiA1OS41NDkyIDE1NS4wMDEgNTkuNzU2OCAxNTQuOTI2IDYwLjAxNDZMMTU0LjkzMiA2NEgxNTMuNjI2VjYwLjE3NThDMTUzLjYwOSA1OS40OTE5IDE1My4yNTkgNTkuMTQ5OSAxNTIuNTc5IDU5LjE0OTlDMTUyLjA1NiA1OS4xNDk5IDE1MS42ODYgNTkuMzYzIDE1MS40NjcgNTkuNzg5MVY2NEgxNTAuMTYyVjU4LjE4ODVIMTUxLjM5MloiIGZpbGw9IiNGRkE1MDAiLz4KPHBhdGggZD0iTTE2Mi4yOTUgNjQuMTA3NEMxNjEuNDY4IDY0LjEwNzQgMTYwLjc5NyA2My44NDc4IDE2MC4yODEgNjMuMzI4NkMxNTkuNzY5IDYyLjgwNTggMTU5LjUxMyA2Mi4xMTEyIDE1OS41MTMgNjEuMjQ0NlY2MS4wODM1QzE1OS41MTMgNjAuNTAzNCAxNTkuNjI0IDU5Ljk4NiAxNTkuODQ2IDU5LjUzMTJDMTYwLjA3MiA1OS4wNzI5IDE2MC4zODcgNTguNzE2NiAxNjAuNzkyIDU4LjQ2MjRDMTYxLjE5NiA1OC4yMDgyIDE2MS42NDcgNTguMDgxMSAxNjIuMTQ1IDU4LjA4MTFDMTYyLjkzNiA1OC4wODExIDE2My41NDcgNTguMzMzNSAxNjMuOTc3IDU4LjgzODRDMTY0LjQxIDU5LjM0MzMgMTY0LjYyNiA2MC4wNTc2IDE2NC42MjYgNjAuOTgxNFY2MS41MDc4SDE2MC44MjlDMTYwLjg2OCA2MS45ODc2IDE2MS4wMjggNjIuMzY3MiAxNjEuMzA3IDYyLjY0NjVDMTYxLjU5IDYyLjkyNTggMTYxLjk0NCA2My4wNjU0IDE2Mi4zNzEgNjMuMDY1NEMxNjIuOTY5IDYzLjA2NTQgMTYzLjQ1NiA2Mi44MjM3IDE2My44MzIgNjIuMzQwM0wxNjQuNTM1IDYzLjAxMTdDMTY0LjMwMiA2My4zNTkgMTYzLjk5MSA2My42Mjk0IDE2My42MDEgNjMuODIyOEMxNjMuMjE0IDY0LjAxMjUgMTYyLjc3OSA2NC4xMDc0IDE2Mi4yOTUgNjQuMTA3NFpNMTYyLjE0IDU5LjEyODRDMTYxLjc4MiA1OS4xMjg0IDE2MS40OTIgNTkuMjUzNyAxNjEuMjcgNTkuNTA0NEMxNjEuMDUxIDU5Ljc1NSAxNjAuOTExIDYwLjEwNDIgMTYwLjg1MSA2MC41NTE4SDE2My4zMzdWNjAuNDU1MUMxNjMuMzA5IDYwLjAxODIgMTYzLjE5MiA1OS42ODg4IDE2Mi45ODggNTkuNDY2OEMxNjIuNzg0IDU5LjI0MTIgMTYyLjUwMSA1OS4xMjg0IDE2Mi4xNCA1OS4xMjg0WiIgZmlsbD0iI0ZGQTUwMCIvPgo8cGF0aCBkPSJNMTY1LjQgNjEuMDQwNUMxNjUuNCA2MC40NzEyIDE2NS41MTMgNTkuOTU5MSAxNjUuNzM4IDU5LjUwNDRDMTY1Ljk2NCA1OS4wNDYxIDE2Ni4yODEgNTguNjk1MSAxNjYuNjg5IDU4LjQ1MTdDMTY3LjA5NyA1OC4yMDQ2IDE2Ny41NjYgNTguMDgxMSAxNjguMDk2IDU4LjA4MTFDMTY4Ljg4IDU4LjA4MTEgMTY5LjUxNiA1OC4zMzM1IDE3MC4wMDMgNTguODM4NEMxNzAuNDkzIDU5LjM0MzMgMTcwLjc1OCA2MC4wMTI5IDE3MC43OTggNjAuODQ3MkwxNzAuODAzIDYxLjE1MzNDMTcwLjgwMyA2MS43MjYyIDE3MC42OTIgNjIuMjM4MyAxNzAuNDcgNjIuNjg5NUMxNzAuMjUyIDYzLjE0MDYgMTY5LjkzNyA2My40ODk3IDE2OS41MjUgNjMuNzM2OEMxNjkuMTE3IDYzLjk4MzkgMTY4LjY0NCA2NC4xMDc0IDE2OC4xMDcgNjQuMTA3NEMxNjcuMjg3IDY0LjEwNzQgMTY2LjYzIDYzLjgzNTMgMTY2LjEzNiA2My4yOTFDMTY1LjY0NSA2Mi43NDMyIDE2NS40IDYyLjAxNDUgMTY1LjQgNjEuMTA1VjYxLjA0MDVaTTE2Ni43MDUgNjEuMTUzM0MxNjYuNzA1IDYxLjc1MTMgMTY2LjgyOSA2Mi4yMjA0IDE2Ny4wNzYgNjIuNTYwNUMxNjcuMzIzIDYyLjg5NzEgMTY3LjY2NyA2My4wNjU0IDE2OC4xMDcgNjMuMDY1NEMxNjguNTQ3IDYzLjA2NTQgMTY4Ljg4OSA2Mi44OTM2IDE2OS4xMzMgNjIuNTQ5OEMxNjkuMzggNjIuMjA2MSAxNjkuNTAzIDYxLjcwMyAxNjkuNTAzIDYxLjA0MDVDMTY5LjUwMyA2MC40NTMzIDE2OS4zNzYgNTkuOTg3OCAxNjkuMTIyIDU5LjY0NEMxNjguODcxIDU5LjMwMDMgMTY4LjUyOSA1OS4xMjg0IDE2OC4wOTYgNTkuMTI4NEMxNjcuNjcgNTkuMTI4NCAxNjcuMzMyIDU5LjI5ODUgMTY3LjA4MSA1OS42Mzg3QzE2Ni44MyA1OS45NzUzIDE2Ni43MDUgNjAuNDgwMSAxNjYuNzA1IDYxLjE1MzNaIiBmaWxsPSIjRkZBNTAwIi8+CjxwYXRoIGQ9Ik0xNzUuNDI4IDYzLjQzMDdDMTc1LjA0NSA2My44ODE4IDE3NC41IDY0LjEwNzQgMTczLjc5NSA2NC4xMDc0QzE3My4xNjUgNjQuMTA3NCAxNzIuNjg3IDYzLjkyMyAxNzIuMzYxIDYzLjU1NDJDMTcyLjAzOSA2My4xODU0IDE3MS44NzcgNjIuNjUxOSAxNzEuODc3IDYxLjk1MzZWNTguMTg4NUgxNzMuMTgzVjYxLjkzNzVDMTczLjE4MyA2Mi42NzUxIDE3My40ODkgNjMuMDQzOSAxNzQuMTAxIDYzLjA0MzlDMTc0LjczNSA2My4wNDM5IDE3NS4xNjMgNjIuODE2NiAxNzUuMzg1IDYyLjM2MThWNTguMTg4NUgxNzYuNjlWNjRIMTc1LjQ2TDE3NS40MjggNjMuNDMwN1oiIGZpbGw9IiNGRkE1MDAiLz4KPHBhdGggZD0iTTE3OS42NTUgNTYuNzc1OVY1OC4xODg1SDE4MC42ODFWNTkuMTU1M0gxNzkuNjU1VjYyLjM5OTRDMTc5LjY1NSA2Mi42MjE0IDE3OS42OTggNjIuNzgyNiAxNzkuNzg0IDYyLjg4MjhDMTc5Ljg3MyA2Mi45Nzk1IDE4MC4wMzEgNjMuMDI3OCAxODAuMjU2IDYzLjAyNzhDMTgwLjQwNyA2My4wMjc4IDE4MC41NTkgNjMuMDA5OSAxODAuNzEzIDYyLjk3NDFWNjMuOTgzOUMxODAuNDE2IDY0LjA2NjIgMTgwLjEyOSA2NC4xMDc0IDE3OS44NTQgNjQuMTA3NEMxNzguODUxIDY0LjEwNzQgMTc4LjM1IDYzLjU1NDIgMTc4LjM1IDYyLjQ0NzhWNTkuMTU1M0gxNzcuMzk0VjU4LjE4ODVIMTc4LjM1VjU2Ljc3NTlIMTc5LjY1NVoiIGZpbGw9IiNGRkE1MDAiLz4KPHBhdGggZD0iTTE0NS4zMDEgMTAwLjY4NkgxNDIuMTU0VjEwNEgxNDAuNzk1Vjk2LjE3OTdIMTQ1Ljc2M1Y5Ny4yNzU0SDE0Mi4xNTRWOTkuNjAxMUgxNDUuMzAxVjEwMC42ODZaIiBmaWxsPSIjRkYzRTNFIi8+CjxwYXRoIGQ9Ik0xNTAuMDA2IDEwNEMxNDkuOTQ5IDEwMy44ODkgMTQ5Ljg5OSAxMDMuNzA4IDE0OS44NTYgMTAzLjQ1OEMxNDkuNDQxIDEwMy44OTEgMTQ4LjkzMiAxMDQuMTA3IDE0OC4zMzEgMTA0LjEwN0MxNDcuNzQ3IDEwNC4xMDcgMTQ3LjI3MSAxMDMuOTQxIDE0Ni45MDIgMTAzLjYwOEMxNDYuNTMzIDEwMy4yNzUgMTQ2LjM0OSAxMDIuODYzIDE0Ni4zNDkgMTAyLjM3M0MxNDYuMzQ5IDEwMS43NTMgMTQ2LjU3OCAxMDEuMjc5IDE0Ny4wMzYgMTAwLjk0OUMxNDcuNDk4IDEwMC42MTYgMTQ4LjE1NyAxMDAuNDUgMTQ5LjAxMyAxMDAuNDVIMTQ5LjgxM1YxMDAuMDY4QzE0OS44MTMgOTkuNzY3NiAxNDkuNzI5IDk5LjUyNzcgMTQ5LjU2MSA5OS4zNDg2QzE0OS4zOTIgOTkuMTY2IDE0OS4xMzYgOTkuMDc0NyAxNDguNzkyIDk5LjA3NDdDMTQ4LjQ5NSA5OS4wNzQ3IDE0OC4yNTIgOTkuMTQ5OSAxNDguMDYyIDk5LjMwMDNDMTQ3Ljg3MiA5OS40NDcxIDE0Ny43NzcgOTkuNjM1MSAxNDcuNzc3IDk5Ljg2NDNIMTQ2LjQ3MkMxNDYuNDcyIDk5LjU0NTYgMTQ2LjU3OCA5OS4yNDg0IDE0Ni43ODkgOTguOTcyN0MxNDcgOTguNjkzNCAxNDcuMjg3IDk4LjQ3NDkgMTQ3LjY0OCA5OC4zMTc0QzE0OC4wMTQgOTguMTU5OCAxNDguNDIgOTguMDgxMSAxNDguODY4IDk4LjA4MTFDMTQ5LjU0OCA5OC4wODExIDE1MC4wOSA5OC4yNTI5IDE1MC40OTUgOTguNTk2N0MxNTAuOSA5OC45MzY4IDE1MS4xMDcgOTkuNDE2NyAxNTEuMTE4IDEwMC4wMzZWMTAyLjY1N0MxNTEuMTE4IDEwMy4xOCAxNTEuMTkyIDEwMy41OTcgMTUxLjMzOCAxMDMuOTA5VjEwNEgxNTAuMDA2Wk0xNDguNTcyIDEwMy4wNkMxNDguODMgMTAzLjA2IDE0OS4wNzIgMTAyLjk5NyAxNDkuMjk3IDEwMi44NzJDMTQ5LjUyNyAxMDIuNzQ3IDE0OS42OTggMTAyLjU3OCAxNDkuODEzIDEwMi4zNjdWMTAxLjI3MUgxNDkuMTA5QzE0OC42MjYgMTAxLjI3MSAxNDguMjYzIDEwMS4zNTYgMTQ4LjAxOSAxMDEuNTI0QzE0Ny43NzYgMTAxLjY5MiAxNDcuNjU0IDEwMS45MyAxNDcuNjU0IDEwMi4yMzhDMTQ3LjY1NCAxMDIuNDg5IDE0Ny43MzYgMTAyLjY4OSAxNDcuOTAxIDEwMi44NEMxNDguMDY5IDEwMi45ODcgMTQ4LjI5MyAxMDMuMDYgMTQ4LjU3MiAxMDMuMDZaIiBmaWxsPSIjRkYzRTNFIi8+CjxwYXRoIGQ9Ik0xNTMuODc0IDEwNEgxNTIuNTY4Vjk4LjE4ODVIMTUzLjg3NFYxMDRaTTE1Mi40ODggOTYuNjc5MkMxNTIuNDg4IDk2LjQ3ODcgMTUyLjU1IDk2LjMxMjIgMTUyLjY3NiA5Ni4xNzk3QzE1Mi44MDUgOTYuMDQ3MiAxNTIuOTg3IDk1Ljk4MSAxNTMuMjI0IDk1Ljk4MUMxNTMuNDYgOTUuOTgxIDE1My42NDMgOTYuMDQ3MiAxNTMuNzcxIDk2LjE3OTdDMTUzLjkgOTYuMzEyMiAxNTMuOTY1IDk2LjQ3ODcgMTUzLjk2NSA5Ni42NzkyQzE1My45NjUgOTYuODc2MSAxNTMuOSA5Ny4wNDA5IDE1My43NzEgOTcuMTczM0MxNTMuNjQzIDk3LjMwMjIgMTUzLjQ2IDk3LjM2NjcgMTUzLjIyNCA5Ny4zNjY3QzE1Mi45ODcgOTcuMzY2NyAxNTIuODA1IDk3LjMwMjIgMTUyLjY3NiA5Ny4xNzMzQzE1Mi41NSA5Ny4wNDA5IDE1Mi40ODggOTYuODc2MSAxNTIuNDg4IDk2LjY3OTJaIiBmaWxsPSIjRkYzRTNFIi8+CjxwYXRoIGQ9Ik0xNTYuNjg4IDEwNEgxNTUuMzgzVjk1Ljc1SDE1Ni42ODhWMTA0WiIgZmlsbD0iI0ZGM0UzRSIvPgo8cGF0aCBkPSJNMTYwLjY3MyAxMDQuMTA3QzE1OS44NDYgMTA0LjEwNyAxNTkuMTc1IDEwMy44NDggMTU4LjY1OSAxMDMuMzI5QzE1OC4xNDcgMTAyLjgwNiAxNTcuODkxIDEwMi4xMTEgMTU3Ljg5MSAxMDEuMjQ1VjEwMS4wODNDMTU3Ljg5MSAxMDAuNTAzIDE1OC4wMDIgOTkuOTg2IDE1OC4yMjQgOTkuNTMxMkMxNTguNDUgOTkuMDcyOSAxNTguNzY1IDk4LjcxNjYgMTU5LjE2OSA5OC40NjI0QzE1OS41NzQgOTguMjA4MiAxNjAuMDI1IDk4LjA4MTEgMTYwLjUyMyA5OC4wODExQzE2MS4zMTQgOTguMDgxMSAxNjEuOTI1IDk4LjMzMzUgMTYyLjM1NCA5OC44Mzg0QzE2Mi43ODggOTkuMzQzMyAxNjMuMDA0IDEwMC4wNTggMTYzLjAwNCAxMDAuOTgxVjEwMS41MDhIMTU5LjIwN0MxNTkuMjQ2IDEwMS45ODggMTU5LjQwNiAxMDIuMzY3IDE1OS42ODUgMTAyLjY0NkMxNTkuOTY4IDEwMi45MjYgMTYwLjMyMiAxMDMuMDY1IDE2MC43NDkgMTAzLjA2NUMxNjEuMzQ3IDEwMy4wNjUgMTYxLjgzMyAxMDIuODI0IDE2Mi4yMDkgMTAyLjM0TDE2Mi45MTMgMTAzLjAxMkMxNjIuNjggMTAzLjM1OSAxNjIuMzY5IDEwMy42MjkgMTYxLjk3OSAxMDMuODIzQzE2MS41OTIgMTA0LjAxMyAxNjEuMTU3IDEwNC4xMDcgMTYwLjY3MyAxMDQuMTA3Wk0xNjAuNTE4IDk5LjEyODRDMTYwLjE2IDk5LjEyODQgMTU5Ljg2OSA5OS4yNTM3IDE1OS42NDcgOTkuNTA0NEMxNTkuNDI5IDk5Ljc1NSAxNTkuMjg5IDEwMC4xMDQgMTU5LjIyOSAxMDAuNTUySDE2MS43MTVWMTAwLjQ1NUMxNjEuNjg3IDEwMC4wMTggMTYxLjU3IDk5LjY4ODggMTYxLjM2NiA5OS40NjY4QzE2MS4xNjIgOTkuMjQxMiAxNjAuODc5IDk5LjEyODQgMTYwLjUxOCA5OS4xMjg0WiIgZmlsbD0iI0ZGM0UzRSIvPgo8cGF0aCBkPSJNMTYzLjc3OCAxMDEuMDUxQzE2My43NzggMTAwLjE1NiAxNjMuOTg2IDk5LjQzODIgMTY0LjQwMSA5OC44OTc1QzE2NC44MTYgOTguMzUzMiAxNjUuMzczIDk4LjA4MTEgMTY2LjA3MSA5OC4wODExQzE2Ni42ODcgOTguMDgxMSAxNjcuMTg1IDk4LjI5NTkgMTY3LjU2NCA5OC43MjU2Vjk1Ljc1SDE2OC44N1YxMDRIMTY3LjY4OEwxNjcuNjI0IDEwMy4zOThDMTY3LjIzMyAxMDMuODcxIDE2Ni43MTIgMTA0LjEwNyAxNjYuMDYxIDEwNC4xMDdDMTY1LjM4IDEwNC4xMDcgMTY0LjgyOSAxMDMuODMzIDE2NC40MDYgMTAzLjI4NkMxNjMuOTg3IDEwMi43MzggMTYzLjc3OCAxMDEuOTkzIDE2My43NzggMTAxLjA1MVpNMTY1LjA4MyAxMDEuMTY0QzE2NS4wODMgMTAxLjc1NSAxNjUuMTk2IDEwMi4yMTcgMTY1LjQyMSAxMDIuNTVDMTY1LjY1MSAxMDIuODc5IDE2NS45NzUgMTAzLjA0NCAxNjYuMzk0IDEwMy4wNDRDMTY2LjkyNyAxMDMuMDQ0IDE2Ny4zMTcgMTAyLjgwNiAxNjcuNTY0IDEwMi4zM1Y5OS44NDgxQzE2Ny4zMjUgOTkuMzgyNiAxNjYuOTM4IDk5LjE0OTkgMTY2LjQwNCA5OS4xNDk5QzE2NS45ODIgOTkuMTQ5OSAxNjUuNjU2IDk5LjMxODIgMTY1LjQyNyA5OS42NTQ4QzE2NS4xOTggOTkuOTg3OCAxNjUuMDgzIDEwMC40OTEgMTY1LjA4MyAxMDEuMTY0WiIgZmlsbD0iI0ZGM0UzRSIvPgo8cGF0aCBkPSJNMTQwLjc5NSAxNDRWMTM2LjE4SDE0My4xMDRDMTQzLjc5NiAxMzYuMTggMTQ0LjQwOCAxMzYuMzM0IDE0NC45NDEgMTM2LjY0MkMxNDUuNDc5IDEzNi45NSAxNDUuODk0IDEzNy4zODYgMTQ2LjE4OCAxMzcuOTUyQzE0Ni40ODEgMTM4LjUxOCAxNDYuNjI4IDEzOS4xNjYgMTQ2LjYyOCAxMzkuODk2VjE0MC4yODlDMTQ2LjYyOCAxNDEuMDMgMTQ2LjQ3OSAxNDEuNjgxIDE0Ni4xODIgMTQyLjI0NEMxNDUuODg5IDE0Mi44MDYgMTQ1LjQ2OCAxNDMuMjM5IDE0NC45MiAxNDMuNTQzQzE0NC4zNzYgMTQzLjg0OCAxNDMuNzUxIDE0NCAxNDMuMDQ1IDE0NEgxNDAuNzk1Wk0xNDIuMTU0IDEzNy4yNzVWMTQyLjkxNUgxNDMuMDRDMTQzLjc1MyAxNDIuOTE1IDE0NC4yOTkgMTQyLjY5MyAxNDQuNjc4IDE0Mi4yNDlDMTQ1LjA2MSAxNDEuODAxIDE0NS4yNTcgMTQxLjE2IDE0NS4yNjQgMTQwLjMyNlYxMzkuODkxQzE0NS4yNjQgMTM5LjA0MiAxNDUuMDc5IDEzOC4zOTQgMTQ0LjcxIDEzNy45NDdDMTQ0LjM0MiAxMzcuNDk5IDE0My44MDYgMTM3LjI3NSAxNDMuMTA0IDEzNy4yNzVIMTQyLjE1NFoiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE1MC40MTUgMTQ0LjEwN0MxNDkuNTg3IDE0NC4xMDcgMTQ4LjkxNiAxNDMuODQ4IDE0OC40IDE0My4zMjlDMTQ3Ljg4OCAxNDIuODA2IDE0Ny42MzIgMTQyLjExMSAxNDcuNjMyIDE0MS4yNDVWMTQxLjA4M0MxNDcuNjMyIDE0MC41MDMgMTQ3Ljc0MyAxMzkuOTg2IDE0Ny45NjUgMTM5LjUzMUMxNDguMTkxIDEzOS4wNzMgMTQ4LjUwNiAxMzguNzE3IDE0OC45MTEgMTM4LjQ2MkMxNDkuMzE1IDEzOC4yMDggMTQ5Ljc2NiAxMzguMDgxIDE1MC4yNjQgMTM4LjA4MUMxNTEuMDU2IDEzOC4wODEgMTUxLjY2NiAxMzguMzMzIDE1Mi4wOTYgMTM4LjgzOEMxNTIuNTI5IDEzOS4zNDMgMTUyLjc0NiAxNDAuMDU4IDE1Mi43NDYgMTQwLjk4MVYxNDEuNTA4SDE0OC45NDhDMTQ4Ljk4OCAxNDEuOTg4IDE0OS4xNDcgMTQyLjM2NyAxNDkuNDI2IDE0Mi42NDZDMTQ5LjcwOSAxNDIuOTI2IDE1MC4wNjQgMTQzLjA2NSAxNTAuNDkgMTQzLjA2NUMxNTEuMDg4IDE0My4wNjUgMTUxLjU3NSAxNDIuODI0IDE1MS45NTEgMTQyLjM0TDE1Mi42NTQgMTQzLjAxMkMxNTIuNDIyIDE0My4zNTkgMTUyLjExIDE0My42MjkgMTUxLjcyIDE0My44MjNDMTUxLjMzMyAxNDQuMDEzIDE1MC44OTggMTQ0LjEwNyAxNTAuNDE1IDE0NC4xMDdaTTE1MC4yNTkgMTM5LjEyOEMxNDkuOTAxIDEzOS4xMjggMTQ5LjYxMSAxMzkuMjU0IDE0OS4zODkgMTM5LjUwNEMxNDkuMTcgMTM5Ljc1NSAxNDkuMDMxIDE0MC4xMDQgMTQ4Ljk3IDE0MC41NTJIMTUxLjQ1N1YxNDAuNDU1QzE1MS40MjggMTQwLjAxOCAxNTEuMzEyIDEzOS42ODkgMTUxLjEwNyAxMzkuNDY3QzE1MC45MDMgMTM5LjI0MSAxNTAuNjIgMTM5LjEyOCAxNTAuMjU5IDEzOS4xMjhaIiBmaWxsPSIjNENBRjUwIi8+CjxwYXRoIGQ9Ik0xNTUuMTUyIDE0NEgxNTMuODQ3VjEzNS43NUgxNTUuMTUyVjE0NFoiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE1Ny45NjYgMTQ0SDE1Ni42NjFWMTM4LjE4OEgxNTcuOTY2VjE0NFpNMTU2LjU4MSAxMzYuNjc5QzE1Ni41ODEgMTM2LjQ3OSAxNTYuNjQzIDEzNi4zMTIgMTU2Ljc2OSAxMzYuMThDMTU2Ljg5NyAxMzYuMDQ3IDE1Ny4wOCAxMzUuOTgxIDE1Ny4zMTYgMTM1Ljk4MUMxNTcuNTUzIDEzNS45ODEgMTU3LjczNSAxMzYuMDQ3IDE1Ny44NjQgMTM2LjE4QzE1Ny45OTMgMTM2LjMxMiAxNTguMDU4IDEzNi40NzkgMTU4LjA1OCAxMzYuNjc5QzE1OC4wNTggMTM2Ljg3NiAxNTcuOTkzIDEzNy4wNDEgMTU3Ljg2NCAxMzcuMTczQzE1Ny43MzUgMTM3LjMwMiAxNTcuNTUzIDEzNy4zNjcgMTU3LjMxNiAxMzcuMzY3QzE1Ny4wOCAxMzcuMzY3IDE1Ni44OTcgMTM3LjMwMiAxNTYuNzY5IDEzNy4xNzNDMTU2LjY0MyAxMzcuMDQxIDE1Ni41ODEgMTM2Ljg3NiAxNTYuNTgxIDEzNi42NzlaIiBmaWxsPSIjNENBRjUwIi8+CjxwYXRoIGQ9Ik0xNjEuNDQxIDE0Mi4zNDZMMTYyLjY3MSAxMzguMTg4SDE2NC4wMkwxNjIuMDA1IDE0NEgxNjAuODcyTDE1OC44NDIgMTM4LjE4OEgxNjAuMTk1TDE2MS40NDEgMTQyLjM0NloiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE2Ny4zMjMgMTQ0LjEwN0MxNjYuNDk2IDE0NC4xMDcgMTY1LjgyNCAxNDMuODQ4IDE2NS4zMDkgMTQzLjMyOUMxNjQuNzk3IDE0Mi44MDYgMTY0LjU0MSAxNDIuMTExIDE2NC41NDEgMTQxLjI0NVYxNDEuMDgzQzE2NC41NDEgMTQwLjUwMyAxNjQuNjUyIDEzOS45ODYgMTY0Ljg3NCAxMzkuNTMxQzE2NS4wOTkgMTM5LjA3MyAxNjUuNDE0IDEzOC43MTcgMTY1LjgxOSAxMzguNDYyQzE2Ni4yMjMgMTM4LjIwOCAxNjYuNjc1IDEzOC4wODEgMTY3LjE3MiAxMzguMDgxQzE2Ny45NjQgMTM4LjA4MSAxNjguNTc0IDEzOC4zMzMgMTY5LjAwNCAxMzguODM4QzE2OS40MzcgMTM5LjM0MyAxNjkuNjU0IDE0MC4wNTggMTY5LjY1NCAxNDAuOTgxVjE0MS41MDhIMTY1Ljg1NkMxNjUuODk2IDE0MS45ODggMTY2LjA1NSAxNDIuMzY3IDE2Ni4zMzQgMTQyLjY0NkMxNjYuNjE3IDE0Mi45MjYgMTY2Ljk3MiAxNDMuMDY1IDE2Ny4zOTggMTQzLjA2NUMxNjcuOTk2IDE0My4wNjUgMTY4LjQ4MyAxNDIuODI0IDE2OC44NTkgMTQyLjM0TDE2OS41NjIgMTQzLjAxMkMxNjkuMzMgMTQzLjM1OSAxNjkuMDE4IDE0My42MjkgMTY4LjYyOCAxNDMuODIzQzE2OC4yNDEgMTQ0LjAxMyAxNjcuODA2IDE0NC4xMDcgMTY3LjMyMyAxNDQuMTA3Wk0xNjcuMTY3IDEzOS4xMjhDMTY2LjgwOSAxMzkuMTI4IDE2Ni41MTkgMTM5LjI1NCAxNjYuMjk3IDEzOS41MDRDMTY2LjA3OCAxMzkuNzU1IDE2NS45MzkgMTQwLjEwNCAxNjUuODc4IDE0MC41NTJIMTY4LjM2NVYxNDAuNDU1QzE2OC4zMzYgMTQwLjAxOCAxNjguMjIgMTM5LjY4OSAxNjguMDE2IDEzOS40NjdDMTY3LjgxMiAxMzkuMjQxIDE2Ny41MjkgMTM5LjEyOCAxNjcuMTY3IDEzOS4xMjhaIiBmaWxsPSIjNENBRjUwIi8+CjxwYXRoIGQ9Ik0xNzMuNzE0IDEzOS4zODFDMTczLjU0MiAxMzkuMzUyIDE3My4zNjUgMTM5LjMzOCAxNzMuMTgzIDEzOS4zMzhDMTcyLjU4NSAxMzkuMzM4IDE3Mi4xODIgMTM5LjU2NyAxNzEuOTc0IDE0MC4wMjVWMTQ0SDE3MC42NjlWMTM4LjE4OEgxNzEuOTE1TDE3MS45NDcgMTM4LjgzOEMxNzIuMjYyIDEzOC4zMzMgMTcyLjY5OSAxMzguMDgxIDE3My4yNTggMTM4LjA4MUMxNzMuNDQ0IDEzOC4wODEgMTczLjU5OCAxMzguMTA2IDE3My43MiAxMzguMTU2TDE3My43MTQgMTM5LjM4MVoiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE3Ni45OTEgMTQ0LjEwN0MxNzYuMTY0IDE0NC4xMDcgMTc1LjQ5MiAxNDMuODQ4IDE3NC45NzcgMTQzLjMyOUMxNzQuNDY1IDE0Mi44MDYgMTc0LjIwOCAxNDIuMTExIDE3NC4yMDggMTQxLjI0NVYxNDEuMDgzQzE3NC4yMDggMTQwLjUwMyAxNzQuMzE5IDEzOS45ODYgMTc0LjU0MiAxMzkuNTMxQzE3NC43NjcgMTM5LjA3MyAxNzUuMDgyIDEzOC43MTcgMTc1LjQ4NyAxMzguNDYyQzE3NS44OTEgMTM4LjIwOCAxNzYuMzQzIDEzOC4wODEgMTc2Ljg0IDEzOC4wODFDMTc3LjYzMiAxMzguMDgxIDE3OC4yNDIgMTM4LjMzMyAxNzguNjcyIDEzOC44MzhDMTc5LjEwNSAxMzkuMzQzIDE3OS4zMjIgMTQwLjA1OCAxNzkuMzIyIDE0MC45ODFWMTQxLjUwOEgxNzUuNTI0QzE3NS41NjQgMTQxLjk4OCAxNzUuNzIzIDE0Mi4zNjcgMTc2LjAwMiAxNDIuNjQ2QzE3Ni4yODUgMTQyLjkyNiAxNzYuNjQgMTQzLjA2NSAxNzcuMDY2IDE0My4wNjVDMTc3LjY2NCAxNDMuMDY1IDE3OC4xNTEgMTQyLjgyNCAxNzguNTI3IDE0Mi4zNEwxNzkuMjMgMTQzLjAxMkMxNzguOTk4IDE0My4zNTkgMTc4LjY4NiAxNDMuNjI5IDE3OC4yOTYgMTQzLjgyM0MxNzcuOTA5IDE0NC4wMTMgMTc3LjQ3NCAxNDQuMTA3IDE3Ni45OTEgMTQ0LjEwN1pNMTc2LjgzNSAxMzkuMTI4QzE3Ni40NzcgMTM5LjEyOCAxNzYuMTg3IDEzOS4yNTQgMTc1Ljk2NSAxMzkuNTA0QzE3NS43NDYgMTM5Ljc1NSAxNzUuNjA3IDE0MC4xMDQgMTc1LjU0NiAxNDAuNTUySDE3OC4wMzNWMTQwLjQ1NUMxNzguMDA0IDE0MC4wMTggMTc3Ljg4OCAxMzkuNjg5IDE3Ny42ODQgMTM5LjQ2N0MxNzcuNDc5IDEzOS4yNDEgMTc3LjE5NyAxMzkuMTI4IDE3Ni44MzUgMTM5LjEyOFoiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE4MC4wOTUgMTQxLjA1MUMxODAuMDk1IDE0MC4xNTYgMTgwLjMwMyAxMzkuNDM4IDE4MC43MTggMTM4Ljg5N0MxODEuMTM0IDEzOC4zNTMgMTgxLjY5IDEzOC4wODEgMTgyLjM4OSAxMzguMDgxQzE4My4wMDUgMTM4LjA4MSAxODMuNTAyIDEzOC4yOTYgMTgzLjg4MiAxMzguNzI2VjEzNS43NUgxODUuMTg3VjE0NEgxODQuMDA1TDE4My45NDEgMTQzLjM5OEMxODMuNTUxIDE0My44NzEgMTgzLjAzIDE0NC4xMDcgMTgyLjM3OCAxNDQuMTA3QzE4MS42OTggMTQ0LjEwNyAxODEuMTQ2IDE0My44MzMgMTgwLjcyNCAxNDMuMjg2QzE4MC4zMDUgMTQyLjczOCAxODAuMDk1IDE0MS45OTMgMTgwLjA5NSAxNDEuMDUxWk0xODEuNCAxNDEuMTY0QzE4MS40IDE0MS43NTUgMTgxLjUxMyAxNDIuMjE3IDE4MS43MzkgMTQyLjU1QzE4MS45NjggMTQyLjg3OSAxODIuMjkyIDE0My4wNDQgMTgyLjcxMSAxNDMuMDQ0QzE4My4yNDQgMTQzLjA0NCAxODMuNjM1IDE0Mi44MDYgMTgzLjg4MiAxNDIuMzNWMTM5Ljg0OEMxODMuNjQyIDEzOS4zODMgMTgzLjI1NSAxMzkuMTUgMTgyLjcyMiAxMzkuMTVDMTgyLjI5OSAxMzkuMTUgMTgxLjk3MyAxMzkuMzE4IDE4MS43NDQgMTM5LjY1NUMxODEuNTE1IDEzOS45ODggMTgxLjQgMTQwLjQ5MSAxODEuNCAxNDEuMTY0WiIgZmlsbD0iIzRDQUY1MCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE5NiAxSDRDMi4zNDMxNCAxIDEgMi4zNDMxNCAxIDRWMTU2QzEgMTU3LjY1NyAyLjM0MzE0IDE1OSA0IDE1OUgxOTZDMTk3LjY1NyAxNTkgMTk5IDE1Ny42NTcgMTk5IDE1NlY0QzE5OSAyLjM0MzE1IDE5Ny42NTcgMSAxOTYgMVpNNCAwSDE5NkMxOTguMjA5IDAgMjAwIDEuNzkwODYgMjAwIDRWMTU2QzIwMCAxNTguMjA5IDE5OC4yMDkgMTYwIDE5NiAxNjBINEMxLjc5MDg2IDE2MCAwIDE1OC4yMDkgMCAxNTZWNEMwIDEuNzkwODYgMS43OTA4NiAwIDQgMFoiIGZpbGw9IiNFMEUwRTAiLz4KPC9zdmc+Cg==", - "description": "Displays Persistent RPC requests that match selected alias and filter with the ability of pagination and sending persistent RPC requests.", - "descriptor": { - "type": "rpc", - "sizeX": 7.5, - "sizeY": 4, - "resources": [], - "templateHtml": "", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-persistent-table-widget-settings", - "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableStickyAction\":true,\"enableFilter\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"enableStickyHeader\":true,\"displayColumns\":[\"rpcId\",\"messageType\",\"status\",\"method\",\"createdTime\",\"expirationTime\"],\"displayDetails\":true,\"defaultSortOrder\":\"-createdTime\",\"allowSendRequest\":true,\"allowDelete\":true},\"title\":\"Persistent RPC table\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px\"},\"targetDeviceAliasIds\":[]}" - } - }, - { - "alias": "slide_toggle_control", - "name": "Slide Toggle Control", - "image": "data:image/png;base64,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", - "description": "Allows to send the RPC call to device when user toggle the slider. Advanced widget settings allow you to configure how to fetch the initial value of the slider.", - "descriptor": { - "type": "rpc", - "sizeX": 3, - "sizeY": 1, - "resources": [], - "templateHtml": "", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-slide-toggle-widget-settings", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":false,\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\",\"title\":\"Slide toggle control\",\"retrieveValueMethod\":\"rpc\",\"valueKey\":\"value\",\"parseValueFunction\":\"return data ? true : false;\",\"convertValueFunction\":\"return value;\",\"requestPersistent\":false,\"labelPosition\":\"after\",\"sliderColor\":\"accent\"},\"title\":\"Slide Toggle Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2,\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\"}" - } - } + "widgetTypeFqns": [ + "control_widgets.switch_control", + "control_widgets.slide_toggle_control", + "control_widgets.round_switch", + "control_widgets.led_indicator", + "control_widgets.rpcbutton", + "control_widgets.knob_control", + "control_widgets.update_attributes", + "control_widgets.persistent_table", + "control_widgets.rpc_debug_terminal", + "control_widgets.rpc_remote_shell", + "gpio_widgets.basic_gpio_control", + "gpio_widgets.raspberry_pi_gpio_control" ] -} +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/date.json b/application/src/main/data/json/system/widget_bundles/date.json index dc868235df..be52525df1 100644 --- a/application/src/main/data/json/system/widget_bundles/date.json +++ b/application/src/main/data/json/system/widget_bundles/date.json @@ -3,27 +3,11 @@ "alias": "date", "title": "Date", "image": "data:image/png;base64,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", - "description": "Contains widgets to change the data range for other widgets on the dashboard." + "description": "Contains widgets to change the data range for other widgets on the dashboard.", + "externalId": null, + "name": "Date" }, - "widgetTypes": [ - { - "alias": "date_range_navigator", - "name": "Date-range-navigator", - "image": "data:image/png;base64,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", - "description": "Allows to change the data range for other widgets on the dashboard.", - "descriptor": { - "type": "static", - "sizeX": 5, - "sizeY": 5.5, - "resources": [], - "templateHtml": "", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-date-range-navigator-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"defaultInterval\":\"week\",\"stepSize\":\"day\"},\"title\":\"Date-range-navigator\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - } + "widgetTypeFqns": [ + "date.date_range_navigator" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/digital_gauges.json b/application/src/main/data/json/system/widget_bundles/digital_gauges.json index 7974ae2ea9..2113a7c508 100644 --- a/application/src/main/data/json/system/widget_bundles/digital_gauges.json +++ b/application/src/main/data/json/system/widget_bundles/digital_gauges.json @@ -2,256 +2,24 @@ "widgetsBundle": { "alias": "digital_gauges", "title": "Digital gauges", - "image": "data:image/png;base64,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", - "description": "Display temperature, humidity, speed, and other latest values on various digital gauge widgets." + "image": "data:image/png;base64,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", + "description": "Display temperature, humidity, speed, and other latest values on various digital gauge widgets.", + "externalId": null, + "name": "Digital gauges" }, - "widgetTypes": [ - { - "alias": "gauge_justgage", - "name": "Gauge", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display any value reading. Allows to configure value range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 4, - "sizeY": 3, - "resources": [], - "templateHtml": "", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":36,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"arc\"},\"title\":\"Gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "digital_thermometer", - "name": "Digital thermometer", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display temperature. Allows to configure temperature range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 3, - "sizeY": 3, - "resources": [], - "templateHtml": "", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < -60) {\\n\\tvalue = 60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[\"#304ffe\",\"#7e57c2\",\"#ff4081\",\"#d32f2f\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"dashThickness\":1.5,\"minValue\":-60,\"gaugeColor\":\"#333333\",\"neonGlowBrightness\":35,\"gaugeType\":\"donut\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital thermometer\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "digital_speedometer", - "name": "Digital speedometer", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display speed. Allows to configure speed range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 5, - "sizeY": 3, - "resources": [], - "templateHtml": "", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 45) {\\n\\tvalue = 45;\\n} else if (value > 130) {\\n\\tvalue = 130;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"arc\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital speedometer\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "neon_gauge_justgage", - "name": "Neon gauge", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display any value reading as an arc. Allows to configure value range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 5, - "sizeY": 3, - "resources": [], - "templateHtml": "", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":70,\"dashThickness\":1,\"gaugeType\":\"arc\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Neon gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "lcd_gauge", - "name": "LCD gauge", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display any value reading as an arc. Allows to configure value range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 5, - "sizeY": 3, - "resources": [], - "templateHtml": "", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 180) {\\n\\tvalue = 180;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"arc\"},\"title\":\"LCD gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "lcd_bar_gauge", - "name": "LCD bar gauge", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display any value reading as a bar. Allows to configure value range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 2, - "sizeY": 3.5, - "resources": [], - "templateHtml": "", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"400\",\"size\":16},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"verticalBar\",\"units\":\"%\"},\"title\":\"LCD bar gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "simple_neon_gauge_justgage", - "name": "Simple neon gauge", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display any value reading as a doughnut. Allows to configure value range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 3, - "sizeY": 3, - "resources": [], - "templateHtml": "", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#388e3c\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"gaugeType\":\"donut\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Simple neon gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "vertical_bar_justgage", - "name": "Vertical bar", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABQVBMVEVqampra2tsbGxtbW1ubm5vb29wcHBycnJzc3N0dHR2dnZ3d3d5eXl6enp7e3t8fHx+fn6BgYGDg4OFhYWHh4eIiIiKioqNjY2Ojo6RkZGTk5OWlpaZmZmbm5uenp6fn5+goKCkpKSlpaWmpqanp6eoqKiqqqqrq6usrKytra2xsbGysrK0tLS1tbW2tra4uLi6urq7u7u9vb2+vr7AwMDBwcHExMTFxcXHx8fIyMjJycnKysrMzMzNzc3Ozs7Pz8/Q0NDR0dHS0tLT09PU1NTV1dXX19fY2NjZ2dnb29vc3Nzd3d3f39/g4ODh4eHj4+Pk5OTl5eXm5ubn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fHy8vLz8/P09PT1fAD19fX29vb39/f6+vr7+/v8/Pz9/f3+/v7///8uFkw3AAAAAWJLR0RqJWKVDgAAAkBJREFUeNrt3ElTE3EQhvGebKMIiUExGkSNuCEiKAqKRKJsIuISN0giwQFC5v3+H8BD5uDBE2WV9tTTp1x/lTyd/6lNKRkDAgQIECBA/jtIHEXx4FOv3fMMWTSLJOn9eN4+OoZ0zg4gjZxduvXDMWQim7FIaoe5hutGXtn90CJp2mZcx340cv44tEgas9kLufKyW8ht21BokVSwTPWKBdtOIdvBDSm0SH2zFWnaxn1CeqPB03o9b88+9C2IpV0r+YTsWDJ3NWQdqWNDPiGH9Xp98I3ous1Ji1b1/NYKLZI+FzKTd3LBG/cQbZXNii9T8frttGKe8UCAAJGaS5J6j2/O96TuzOTz2CfkxZhVJF0tTY1MqF++eK/w0Cdkar5akdrBa60EnTXb04NzXn9a1yrSln1V094+yUvLduQYsmp7+m5rs2ekhrUdQ9ZtV99sYy6UGrbvGPLJdvTOmkvBsRay8T+CtE45v0NOirVurdRvZR/tX67KMUSbwza8KS2ENvrFKySZA0lSfMgTBUjqIH+pESBAiB0IjQBhawEBQuxAaAQIWwsIEGIHQiNA2FpAgBA7EBoBwtYCAoTYgdAIELYWECDEDoRGgKQK8vOUAwQIsQOhESBsLWIHQuxAaAQIW4vYgRA7EBoBwtYidiDEDoRGgLC1iB0IsQOhESBsrXTHnlws9g8ZXCz230hysdg/JLlY7B+SXCz230hysdg/JLlY7B9yUqx1a8V+CtZvcrE4Df/sBzxRgAABAgQIECB/mF+DKtRemhqPQgAAAABJRU5ErkJggg==", - "description": "Preconfigured gauge to display any value reading as a bar. Allows to configure value range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 2, - "sizeY": 3.5, - "resources": [], - "templateHtml": "", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":12,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":1.5,\"gaugeColor\":\"#eeeeee\",\"showTitle\":false,\"gaugeType\":\"verticalBar\"},\"title\":\"Vertical bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "simple_gauge_justgage", - "name": "Simple gauge", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAACRlBMVEVmZmZnZ2doaGhpaWlqampra2tsbGxtbW1ubm5wcHBxcXFzc3N0dHR5eXl6enp7e3t8fHx9fX1/f3+AgICDg4OFhYWGhoaIiIiJiYmKioqMjIyOjo6Pj4+QkJCSkpKTk5OWlpaYmJicnJyenp6fn5+mpqaoqKiqqqqrq6usrKytra2vr6+xsbGysrKzs7O1tbW2tra4uLi5ubm6urq7u7u8vLy9vb3BwcHExMTFxcXGxsbIyMjKysrLy8vQ0NDU1NTV1dXW1tbX19fZ2dng4ODh4eHi4uLk5OTl5eXm5ubn5+fo6Ojp6enr6+vs7OzuhzHuhzLunVvutYbutofuwZzuzbLu1cDu18Tu3c/u39Lu49vu6ubu6ufu7Ovu7u7vbADvbQHvbQLvbgPvbgTvbwXvcAfvcAjvcQnvcQrvcgzvdRDvdxPveBbveBfveRjvfiDvfyPvgCTvhi/vhjDviDPvj0DvkUTvkkbvqnHvqnLvwp3vzK/v7+/wgijwijbwjDrwmFDwmVHw8PDxlEfxnFbxnVjxnlrxn1vxn1zxomDxqnDx8fHyp2ryqWzyrHLyrXPysXvy8vLz8/P0uYj0uYn0uov0vI709PT1wJX1wpj1xZ31yaX19fX2yqb2y6f2zKr2zav2zq320LH29vb307b31Lf39/f4+Pj53sj55dT5+fn64Mv65tb659j66Nr66t3669/6+vr76Nj77N/7+/v88+v8/Pz99e799e/99/H9/f3++/n+/Pr+/v7//Pn//v3//v7///+CVUqIAAAAAWJLR0TBZGbvbgAAB7BJREFUeNrdnfef1EQUwAMe9naiCB5SFEWKWM92HigqUsTelckhVrAgZzeKroolWDaWKK6NaFZijcYRoxHyn3k7e2X3bjeZN/Nmds374bjwuczOd2fem/emvDFSNZLQsOq5TrlkWRYZ2rrtiSeHX3v7g8/27vtH0QcaChhC37VJo5gN8sCONz/8dn/XgyRBpZlhCgiT+4Z3f/Fn94JQv0xaitlKtuz8+NduBKG+TdqJ2UaGXv7oly4DCRySIWZ7GXr687+7BiT2LEIEQUbksfd/7woQ6pI8MXNky64fOw7CgZEPYpqbX93XURDqEIICMmKRX/+pYyCJRwgaiGnev/uvzoBULYIKYpqP7zmoHyQuE4INYpqvUN0gPiEqQG61fK0gkOaAgDwz8sflWB9IYBElIA/dWftrK9AFUiFEDcjNo3/vJjpAgN0KAHLDprEX7Fg9SGQRRSBbbp94w4pUgwQCHJwgzzW9E6gF8QlRBbL9nuaXfJUgHlEGMvTi5Lc8dSAVog7kpqmvVVSBuEQdyLa7W7znqgEBt4fteNUgjCj9+Yfvv/v6k/feeOrhtiAvtCyhogIEpB92JWg1FBz47ct3hje34Hi2TTkePgi/vbJaQ4zLH1+9tXUSxyN3tSvMxwYJeDGcgMO/+HfvrgcbQW5pX16ACxLxUZSq3F7S/k9fGue4cVNG+0aYIDHXeG7DRuMD3+wYjXHvyOypMR5IYvNghGBH72Ad5fnsgst4IByG16oKhUMH9zxqbr83p2wXC4RD0StJKij7370tt/QAByRfQewolZAot+PyqAkHSG4g5SaplCS5XbeMAZI3EoqF2MAox5cHoXndiqYIQnO6l0WlQXI6VjlJUSRxJDtXHkgVwTKiBAlVOZAku/NWUkTJdq+tRAok2554aaqPxJMBofraI/9roxIgjib94NITVxyEyvtyQHGEm8QQLdZOVIBk+tmOKAiVGqEER0ZLsEkMwR4bpIokENSSDJBYp8HiM12xEIinW0Hy1cQTAcka1KNUoURCw7sh0lkrqVKpiKimIeD2WolakIy+UIaDUHFHVFqqAupuwANDO1UuNjxUNOBlhepBAvi3aIB7loYGyfoaKRDE1z+m8zWJDwRpa7NKqRYpQe1WG5CkcyYrz3AlIJAAWg76WALt2gZwcHWAFVq7Yv7MY0+cf856tBCrAgKxcWzv+nnGmMy7HkndbQhI24a1QJUZmGFMyGGrkPyUBAASoriLA9OMRpmxGmciIgSA+BiDyLp6exzSd/rcHvbbUS5K3/IBIK5AiDZFTmW176v1xtJc9vsiCEgMC3gNkK5D3JNrWN1n1TtCdHLtYRpI4WGVMEC6DlGRM1nV14yZ4em1x+Uo8VXCDUIxVKS3VvM544/za4+9KEpCuUFCBBUJmMk6e/z5EtZAEHWnILNlgBwdQDU2MBUZGH9ew56vgDQJyN0zIBNBEF2/mlV85YQhZM8XY2i7xw3iIjhaa1nFByesKXs+D8PdcrlBHISFHYtV/PyJ/2A6swwC4kG+TwPSppBYhB5aq/iCSSCLMWKSMjdICSPKPaVW8Z5xtRpkLXQWhv0tcYNYGD78uazmp40+bTyaPa6AlBAqAwFN+ZbrPuNiNnxdWecw+jEmgS1pENjizpJ63Y9b0r901pgrf5leEIICEs40pshKSAEUMi4rBElLDSTTl7F/1uoFwelaI+PRorEYsXf1VcyIRd2hI/D1nWuXzznmyBMWXh6l/TWQ2Wl3gMhMX/c1O8N6zC/KgNgs17HAah3KjBA/CIaLMkkWwnsWgouC4TQ2y4Vw44vhNGK48U1au4xZrz6kWVN+Nx4jsGqoz0XHs/Y4YiPwRfnACiPUrYeJS89Y2Ds6lvSsgr4tH+piTD4wuXRiaD8czIEw+YAyHdQMMnsDuEsiTAehTNA1gpw0KLA+VAF1cHVTpqMgPb0LLtggZCQQpkxxJrFrs/q26Dl7pElsn+A7KWgqAllWCLGUREJQFnqQlt5kBGfprf1iaNDxngVaDMVbnhYWpOXpwmwYKMwWjsJsqinONqfCbDwrzFbA4mzOLMx22cJsYO7glnIXd0t5YTb5F+bYRacOwrjoB2EKczSpOIfFCnN8rzgHKrOPnDp6FV3miGt3HTqOJUDkTmZDReEx8OIczC9MqoTiJK/IHqH+T+lECpPgpTgpd4qTBEltWqqEI/sjUloqlYnCuJK8co1WfDnoeFL82gJKH/DkhENM3aYmmR4fBmoyPe50xYD0hkm1xFUkbnpD7ISTacid4BU54SQoBaibnQI0Dlz+NM7oKUDhSVlbOhW05R0+GIGCljS5lEZhUPUcG1qCkjS5EomLRQUS7mhJJa2BQ09ybzFRmNxbNN26kChNty6YAF9EFCfAF7uSAC7qryQQuiQCLDouidBhvERmmbRcpALsVtouUlHbvXRebVOzw4oaRfNlQ8oapaz9+qcUciEXd3NIbEXQckUar7GSmR+TvLQO0SF2Y6mqaLlGkCdClp1C1nKxY35ryM+Ea7lqM0fFvRihElouP810q6o4Kyxo19HGvi1A4cdYn6/lguA2g5+PuXtCy5XNLaeLkHdOaLlEu5nB9UMF2z8UgIz2s4ZrzcnIj1LZcb1qSFV93n+CgK954RO4IwAAAABJRU5ErkJggg==", - "description": "Preconfigured gauge to display any value reading as a circle. Allows to configure value range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 2, - "sizeY": 2, - "resources": [], - "templateHtml": "\n", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "\nself.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#ef6c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"gaugeColor\":\"#eeeeee\",\"gaugeType\":\"donut\"},\"title\":\"Simple gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "digital_bar", - "name": "Digital horizontal bar", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display any value reading as a horizontal bar. Allows to configure value range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 6, - "sizeY": 2.5, - "resources": [], - "templateHtml": "", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 80) {\\n\\tvalue = 80;\\n} else if (value > 160) {\\n\\tvalue = 160;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"horizontalBar\",\"showTitle\":false,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital horizontal bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "mini_gauge_justgage", - "name": "Mini gauge", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display any value reading as a circle. Allows to configure value range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 2, - "sizeY": 2, - "resources": [], - "templateHtml": "", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#7cb342\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"roundedLineCap\":true,\"gaugeType\":\"donut\"},\"title\":\"Mini gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "horizontal_bar_justgage", - "name": "Horizontal bar", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display any value reading as a horizontal bar. Allows to configure value range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 7, - "sizeY": 3, - "resources": [], - "templateHtml": "\n", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"horizontalBar\"},\"title\":\"Horizontal bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "digital_vertical_bar", - "name": "Digital vertical bar", - "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display any value reading as a vertical bar. Allows to configure value range, gradient colors and other settings.", - "descriptor": { - "type": "latest", - "sizeX": 2.5, - "sizeY": 4.5, - "resources": [], - "templateHtml": "", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#3d5afe\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":14},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":8,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#cccccc\"},\"neonGlowBrightness\":20,\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"verticalBar\",\"showTitle\":false,\"minValue\":-60,\"dashThickness\":1.2,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital vertical bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - } + "widgetTypeFqns": [ + "digital_gauges.simple_gauge_justgage", + "digital_gauges.vertical_bar_justgage", + "digital_gauges.horizontal_bar_justgage", + "digital_gauges.gauge_justgage", + "digital_gauges.mini_gauge_justgage", + "digital_gauges.digital_thermometer", + "digital_gauges.digital_speedometer", + "digital_gauges.digital_vertical_bar", + "digital_gauges.digital_bar", + "digital_gauges.simple_neon_gauge_justgage", + "digital_gauges.neon_gauge_justgage", + "digital_gauges.lcd_gauge", + "digital_gauges.lcd_bar_gauge" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/edge_widgets.json b/application/src/main/data/json/system/widget_bundles/edge_widgets.json index 5d9c9526fa..590e5936cb 100644 --- a/application/src/main/data/json/system/widget_bundles/edge_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/edge_widgets.json @@ -2,28 +2,12 @@ "widgetsBundle": { "alias": "edge_widgets", "title": "Edge widgets", - "image": "data:image/png;base64,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", - "description": "Widgets to manage ThingsBoard Edge." + "image": "data:image/png;base64,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", + "description": "Widgets to manage ThingsBoard Edge.", + "externalId": null, + "name": "Edge widgets" }, - "widgetTypes": [ - { - "alias": "edges_overview", - "name": "Edge Quick Overview", - "image": "data:image/png;base64,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", - "description": "Overview of entities related to ThingsBoard Edge.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 5, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-edge-quick-overview-widget-settings", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"showTitleIcon\":true,\"titleIcon\":\"router\",\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Edge Quick Overview\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"widgetStyle\":{},\"actions\":{}}" - } - } + "widgetTypeFqns": [ + "edge_widgets.edges_overview" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json index 3348c86d89..0d64bf85a6 100644 --- a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json @@ -3,48 +3,12 @@ "alias": "entity_admin_widgets", "title": "Entity admin widgets", "image": "data:image/png;base64,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", - "description": "Templates of complex widgets that allow to list and create/update/delete devices and assets." + "description": "Templates of complex widgets that allow to list and create/update/delete devices and assets.", + "externalId": null, + "name": "Entity admin widgets" }, - "widgetTypes": [ - { - "alias": "device_admin_table", - "name": "Device admin table", - "image": "data:image/png;base64,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", - "description": "Customized entity table widget with preconfigured actions to create, update and delete devices.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 6.5, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-entities-table-widget-settings", - "dataKeySettingsDirective": "tb-entities-table-key-settings", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityLabel\":false,\"useRowStyleFunction\":false},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Add device

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Device name\\n \\n \\n Device name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\\n}\\n\\nfunction AddDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.addDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addDeviceFormGroup.markAsPristine();\\n let device = {\\n name: vm.addDeviceFormGroup.get('deviceName').value,\\n type: vm.addDeviceFormGroup.get('deviceType').value,\\n label: vm.addDeviceFormGroup.get('deviceLabel').value\\n };\\n deviceService.saveDevice(device).subscribe(\\n function (device) {\\n saveAttributes(device.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit device\",\"icon\":\"edit\",\"useShowWidgetActionFunction\":null,\"showWidgetActionFunction\":\"return true;\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Edit device

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Device name\\n \\n \\n Device name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\\n}\\n\\nfunction EditDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.device = null;\\n vm.attributes = {};\\n \\n vm.editDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editDeviceFormGroup.markAsPristine();\\n if (vm.editDeviceFormGroup.get('deviceType').value !== vm.device.type) {\\n delete vm.device.deviceProfileId;\\n }\\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value,\\n vm.device.type = vm.editDeviceFormGroup.get('deviceType').value,\\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value\\n deviceService.saveDevice(vm.device).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n deviceService.getDevice(entityId.id).subscribe(\\n function (device) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.device = device;\\n vm.editDeviceFormGroup.patchValue(\\n {\\n deviceName: vm.device.name,\\n deviceType: vm.device.type,\\n deviceLabel: vm.device.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"openInSeparateDialog\":false,\"openInPopover\":false,\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteDevice();\\n }\\n }\\n );\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}" - } - }, - { - "alias": "asset_admin_table", - "name": "Asset admin table", - "image": "data:image/png;base64,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", - "description": "Customized entity table widget with preconfigured actions to create, update and delete assets.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 6.5, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-entities-table-widget-settings", - "dataKeySettingsDirective": "tb-entities-table-key-settings", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Add asset

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Asset name\\n \\n \\n Asset name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n customDialog.customDialog(htmlTemplate, AddAssetDialogController).subscribe();\\n}\\n\\nfunction AddAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.addAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addAssetFormGroup.markAsPristine();\\n let asset = {\\n name: vm.addAssetFormGroup.get('assetName').value,\\n type: vm.addAssetFormGroup.get('assetType').value,\\n label: vm.addAssetFormGroup.get('assetLabel').value\\n };\\n assetService.saveAsset(asset).subscribe(\\n function (asset) {\\n saveAttributes(asset.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Edit asset

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Asset name\\n \\n \\n Asset name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n customDialog.customDialog(htmlTemplate, EditAssetDialogController).subscribe();\\n}\\n\\nfunction EditAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.asset = null;\\n vm.attributes = {};\\n \\n vm.editAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editAssetFormGroup.markAsPristine();\\n vm.asset.name = vm.editAssetFormGroup.get('assetName').value,\\n vm.asset.type = vm.editAssetFormGroup.get('assetType').value,\\n vm.asset.label = vm.editAssetFormGroup.get('assetLabel').value\\n assetService.saveAsset(vm.asset).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n assetService.getAsset(entityId.id).subscribe(\\n function (asset) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.asset = asset;\\n vm.editAssetFormGroup.patchValue(\\n {\\n assetName: vm.asset.name,\\n assetType: vm.asset.type,\\n assetLabel: vm.asset.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteAsset();\\n }\\n }\\n );\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}" - } - } + "widgetTypeFqns": [ + "entity_admin_widgets.device_admin_table", + "entity_admin_widgets.asset_admin_table" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/entity_widgets.json b/application/src/main/data/json/system/widget_bundles/entity_widgets.json new file mode 100644 index 0000000000..865d2e11ec --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/entity_widgets.json @@ -0,0 +1,14 @@ +{ + "widgetsBundle": { + "alias": "entity_widgets", + "title": "Entity widgets", + "image": "data:image/png;base64,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", + "description": "Contains entity hierarchy and entity tables.", + "externalId": null, + "name": "Entity widgets" + }, + "widgetTypeFqns": [ + "cards.entities_table", + "cards.entities_hierarchy" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json index dc86c0f003..bcaa77e866 100644 --- a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json @@ -3,65 +3,13 @@ "alias": "gateway_widgets", "title": "Gateway widgets", "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC7lBMVEUwVX8wVn8wVoAxV4AyV4EzWYI1WYI1WoM4XYU7X4Y8YIc+YYg/YolDZYtGaI1HR0dIaY1IaY5Ja49LbJBQcZRScpVUVFRUc5ZVVVVWVlZWdZdXV1dXdphYWFhYdphZWVlaeJpcXFxceptdXV1eXl5fX19gYGBhfp5jY2NkZGRkgKBlZWVmZmZmgqFnZ2dng6JoaGhpaWlqampqhaNqhqRra2tsbGxtbW1ubm5vb29wcHBwiqdxcXFycnJyi6hzc3N0dHR0jqp1dXV2dnZ3d3d3kKt4eHh5eXl6enp7e3t7lK58fHx9fX1+fn5/f3+AgICBgYGCgoKCmbKDg4OEhISFhYWGhoaGnLWHh4eHnbWIiIiInraJiYmJn7eKioqKoLeLi4uMjIyMobiNjY2Ojo6Pj4+QkJCQpLuRkZGRpbuSkpKSpryTk5OUlJSVlZWVqL6Vqb6WlpaWqr+Xl5eXqr+YmJiZmZmZq8Campqbm5ubrcKcnJydnZ2dr8Oenp6er8OesMSfn5+goKChoaGioqKjo6OkpKSlpaWltcimpqanp6eoqKipqamqqqqrq6urususrKytra2tvMyurq6uvc2vr6+vvc6vvs6wsLCxsbGysrKzs7O0tLS1tbW2tra3t7e4uLi5ubm6urq7u7u8vLy9vb2+vr6+yte/v7/AwMDAy9jBwcHCwsLCzdrDw8PDztrExMTFxcXFz9vGxsbG0dzHx8fIyMjI0t3JycnKysrLy8vMzMzM1eDNzc3N1uDOzs7Pz8/Q0NDR0dHR2ePS0tLT09PU1NTV1dXW1tbW3ebW3ubX19fY2NjZ2dnZ4Oja2trb29vc3Nzd3d3e3t7f39/g4ODh4eHi4uLj4+Pk5OTl5eXm5ubn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDw8PHw8vbx8fHy8vLz8/Pz8/T09PT19fX29vb39/f3+Pn4+Pj5+fn5+vz6+vr7+/v8/Pz9/f3+/v7///9UXY26AAAAAWJLR0T5TGRX8AAAC3dJREFUeNrtnXtYW2cdx2Onzrtu3q9TN6tzF0cnXaggsWAgUmiQFI+IeDAUsWtVROqqEbU35Cq11lFoYbR0gxVrEams2SStoaClLuW6pFySnIMmMTnHxHP7/ucfCbSda0lXYJw85/s88PDw8LzJh/O+b973c35vogJPxUA4qHhKguwjUbwqzCHJOGESFRXGEGWbMAqlogFJEgWB5zkZhucFQZQkgFbRkCSR50JBlmVkF5YNhjhelKQwiMiHONmOeIkL8aIEWkVLksCxQfnOWEGWEySJVtGSKIS8cp57vSFBDIPwQVrOIHSQj4BwLCVnEIrlFkDccgZxxyAII28Q5hVA9pB68njLFFCBClRfAluFiYLcHjiLCjuxiySs8BRtbgFQFMRsDrmDnjDkHRJXIQiQAGyzAWqokRHH+3Wiek7YSOeNSIRLDf865FyUCvsx9PBxjBlhJQd2SYf3rujz5ha+LQ7SZDavgxoZByr8ulkDABwkJoHHzM1FUAMXymGczJgHAdQrClLeA/SURwdiamqKgxoZnowB3WQ+TCkjGMrPEdZ3NOklNfDi9mCCY9PU2H35a5mVB6HzTp/Jdd9a1/LMxum4dSJ2XtgrYvslNbCR2RBA09FjmeVkxZgRW0YHdmHEsMKvGwRB3eIY8aBZh47UkrS5Hn3xpmA8aSjHYDKh51P8kNSjRjjSBz6Tq1/pl1OKimaw/3+EEACwC8tNRg7Tb8y8jiggCogCooAoICsK8s9lyr9XIErXUkAUEAVEAVFAlhwkWFZYz5J5BnJ4itD3oJfIrxbBGyWgPY+cQTFJHpAHSMNTqDoLqwlImhLSHPWdqOhAx8NWXNoiTelW2Jfcjtfq3zgAwGrCXAZwvCkMkmXPR00vADxiNr/m4j5Kr/VS2UYaVhNms4GT9fVJqXnSbLojkdl9dobMwP1NTY7XGiQ6r9XsQutRWE0Q4gWUv1DfGVJz+/PL9UdPVQAJq6JrRee1RrNKNk7DagJObczZivpOtNaoBTAbxcK8vDKsJcnaVUASldda8FfCy26QhkSszigviArI8oMoN0NXHUiM3Gfng3RAvhyBq5UPQshzSrYkgS7PfC2KJHC+huqqyv0yTGVVdYMvUh0ESeQDo8NDg7LM0PBoIFKvBUkSOSbg83o9sovX6wsw3HwFXczUNK72KtP/fPGNqhvnHX+QJMyDrO6639+87qZ5V3i7pVr9xXM/WnPzKCAKiAISyd0/+OobYgLkb5//2Q/v+Phdb757zfvX3P0eGYM8+eR713zr53//yF/e9vyjv3/2C/IFueOh57/yuV//9bPPfPvrv332mV/IFuT1//j0d3/150f/9NCX//X2x3/8yY/I94q8+/vfecuHfvK1j73pm2vu/Mbjb1WmXwVEAYkxkN/dnOMu2YD890t33ozjjxEQWbu5ayydKhZOuAISpeJi46yuoAJPUR6Zh6IEqBAjiSEQ5WD+qhrsfOxMvzF0MD/ssLmQLMNFLHb4/ojABdmA3yfD+ANskBMi59lFIcTIeXwwC8fAOUbe59nnCwaEoE/e99l9QUGiVbTIs3PTcgaZnmN5kVbRIhegHHIGcVABLgzid03KGWTS5Q+DhHzOCTmDTDh9oXmQ8flf2k45vwccs58mSJIdNWRbb/dRAnZUXLsIOk8Yx2ZagAo4SeJpBMuJfVI3QZKeHqJ4GtXkrtAtP8T4VZDZBZAzlePvfArlQ1U9gLB+jt0Q5cT8kkWA3+KDw2kR/S6LG+KwA5gYwolc7wTE4Qn4XBYKQDzHZIyWAGpsdEiZjtJu7Ouo6Qa4eJHOObcDfbdeMzk++8og+Ruo8qGqIw7KVgJU9UTV1gDRuIVKb9f4tRXbq3rVzQ9iR7Nh+OgTP91fmz6rQXGjseNkYnMcgMxqL0ZLAHUoCfCx64Ggt2aPeVhKamQwE9/9KgonbwRifFFfPlRFVHYNlgENJ6Nqq3gK9pYONHdo+Vmitx5pzNpK4540SbLbyqHhdOC0Jw9DIwFCh7ZutARQs6lA5Ch8zdam0+DaktpAVakvLx0InrhvqKoH8CUDhujmsx2jON/WitqzWt5J9NZDxyR4vayWEy/YyqARNfBkh0HYWuCxmRxI66EWUD2un8aZjppuwHkEotp8AbbiJQTh7h2q6gHQnL55d5RDREfWBDNLcvl5kGBd7pbL5ix9N/3gixo05KfbIlfEVEiYsKMgqw19acYi6SVtcaa/RkOSzm1bsxvo7O1a69KAXB+ei7o1DteUDAMAJwGiECkX5q52fYEDwAkApBAALFQX88LLm1g6EHlEAVnFIE55g1yzRImNtVbMrH65gNsuZx1kd8/vRxjKMTLY39fTLbv09PUPjjgohhNpFS3xzNz0+CWrxXz2OZnlrNlivTQ+Pcfw83v2GTmPkZn5PXusgEg8I3eLEulaHCN3i8JcnX7lDHJ1+lV00O1GnOGWoJVX1EEDuXnH4M8hyd62PKIVtQyGu9GSZ3QtIh9yC3Zf/5xM1/w8UgoA2MQAgP+a8Zjdo+WXdNF4dRl/olE0tVKbAZRd5LXOTA9O1VqMmNDfvK32Rhw6iJkBCS4zi9D5OUz4KTgZzkqDt5hLAVy5kMEI1nG0Fvh56yTcASCoxbbRZdqPnGgEm0KpzeZg2cEOHZfpwanaX55btK32RjQetBgbnpjJaE3is9pT/BragM3eze0pfqKhsBQYTW15H1N8JG1wX6arsCXF9sIYMENgr2X5QIRE6oGmpkDZ/mM6d6YHp2rLLtjJ7EVA4jPTxNxdleuaunHlYkJlqlkDPUXY1JXavmSMlAL7+rGJmTBlHxmswJgpqw0AEs7Cca+4bCB9OyNdC3v68u042nKsBkhY7IqIiX5ixOs9egIDF4u9Hl6Dp7N6bEVeD58IWylQ3w0dk+hpbx40IcHX0gYAo0bUnl6uKxK/hQhQHyXJrjJdfhZ7ObV4U0AgCgjTol3rTPmItqQ+sGlrsVSw1eDXIHQPD7LI4K/O2VwKUElb72EMW5L3Ox8eycpNrGsfAlgdymwrtWdnASAY5b14LvJ1VUSHMP/+DBIXcT8hCUFAAgDNRDK7UiDLmrn+pVjkKTpI0UGKDlJ0kKKDFB2k6CBFByk6SNFBig5a4YRuu+74BjqIJO07yQILSsiCi5gi9L3oJQpqxMV0EFnqie5hm673Jm3JJeF/66t/t8Eb6CAAarBxghr+OCRO81pHXRdMnTdv60QjBvNgtwj8lQGMnRfhP8cA4KwU7KP0FStcFg72S/4poH2WPUdDvDgJj8sy50k8T3sxctnh6FziPXuJ2Qo12HWcGv44KhNoa44OxK63FNYWOT94yGyoKqYy29MlSNnHtVTcLtsHDlzMbs2Q7t3taAfApbcl0sbDZFd7cnP89IOdhztPGw58asnlQ3ZTOx4hP2HBWvLDthkD0HGgLjm1YJGOfOJ+fcFU6MDuBCcBW0ob+5S+8gE37HGVGSc1oAzYZkexXRP+28E9cFObENS1tyAZOvZwZ64TycuggwA1+nZCjfYqIV5EmaWuK6TmFr0iwN6TYpKTwIirS992yOsRYc/1ejgNKANKbTDQEZBLOzHmSgGV294CDXTs4c6iSSQuvQ4iyTE1kDqjhrjB05ViKEZdF1rqogA5nZj7gJPAkJbYz+pKiiSgxJhDa0AZMJVu3AUN3JUApIKt+mBNQdrwAsjlJON9y71nF2/ls9MiHxgnhRaMUOh6rRSWQGFrdM08aHe4tTEhH/ymYntMgCg6SNFBig5aKR0UI6cVeHZuenzY2i87ifLcc2f7rcPj8+dHJCHoc18Ztw0PDV6QWQaHhm3jVyIneihJ5Bive9oxOTEuu0xMOqbdXoYTJUpFiaIQYnxzlMs5K7s4XdScjwkJokipOEqU/zlEkRJU4N1umqLcLqcs43JTFO12C/gf9yt5MBYZj1cAAAAASUVORK5CYII=", - "description": "Widgets to manage ThingsBoard IoT Gateway." + "description": "Widgets to manage ThingsBoard IoT Gateway.", + "externalId": null, + "name": "Gateway widgets" }, - "widgetTypes": [ - { - "alias": "gateway_configuration", - "name": "Gateway Configuration", - "image": "data:image/png;base64,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", - "description": "Allows to define gateway configuration for a single selected device.", - "descriptor": { - "type": "static", - "sizeX": 8, - "sizeY": 6.5, - "resources": [], - "templateHtml": "\n\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-gateway-config-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"Gateway Configuration\",\"archiveFileName\":\"configurationGateway\"},\"title\":\"Gateway Configuration\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "attributes_card", - "name": "Gateway events", - "image": "data:image/png;base64,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", - "description": "Allows to browse events from the gateway.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 8, - "resources": [], - "templateHtml": "", - "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", - "controllerScript": "let types;\nlet eventsReg = \"eventsReg\";\n\nself.onInit = function() {\n \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n\n if (self.ctx.datasources.length && self.ctx.datasources[0].type === 'entity') {\n getDatasourceKeys(self.ctx.datasources[0]);\n } else {\n processDatasources(self.ctx.datasources);\n }\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, false);\n }\n else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nfunction processDatasources(datasources) {\n var i = 0;\n var tbDatasource = datasources[i];\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n self.onResize();\n}\n\nfunction getDatasourceKeys (datasource) {\n let entityService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('entityService'));\n if (datasource.entityId && datasource.entityType) {\n entityService.getEntityKeys({entityType: datasource.entityType, id: datasource.entityId}, '', 'timeseries').subscribe(\n function(data){\n if (data.length) {\n subscribeForKeys (datasource, data);\n }\n });\n }\n}\n\nfunction subscribeForKeys (datasource, data) {\n let eventsRegVals = self.ctx.settings[eventsReg];\n if (eventsRegVals && eventsRegVals.length > 0) {\n var dataKeys = [];\n data.sort();\n data.forEach(dataValue => {eventsRegVals.forEach(event => {\n if (dataValue.toLowerCase().includes(event.toLowerCase())) {\n var dataKey = {\n type: 'timeseries',\n name: dataValue,\n label: dataValue,\n settings: {},\n _hash: Math.random()\n };\n dataKeys.push(dataKey);\n }\n })});\n\n if (dataKeys.length) {\n updateSubscription (datasource, dataKeys);\n }\n }\n}\n\nfunction updateSubscription (datasource, dataKeys) {\n var datasources = [\n {\n type: 'entity',\n name: datasource.aliasName,\n aliasName: datasource.aliasName,\n entityAliasId: datasource.entityAliasId,\n dataKeys: dataKeys\n }\n ];\n \n var subscriptionOptions = {\n datasources: datasources,\n useDashboardTimewindow: false,\n type: 'latest',\n callbacks: {\n onDataUpdated: (subscription) => {\n self.ctx.data = subscription.data;\n self.onDataUpdated();\n }\n }\n };\n \n processDatasources(datasources);\n self.ctx.subscriptionApi.createSubscription(subscriptionOptions, true).subscribe(\n (subscription) => {\n self.ctx.defaultSubscription = subscription;\n }\n );\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-gateway-events-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Function Math.round\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.826503672916844,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"eventsTitle\":\"Gateway Events Form\",\"eventsReg\":[]},\"title\":\"Gateway events\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "config_form_latest", - "name": "Gateway configuration (Single device)", - "image": "data:image/png;base64,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", - "description": "Allows to create or choose the gateway device and edit the configuration.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 9, - "resources": [], - "templateHtml": "\n", - "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", - "controllerScript": "self.onInit = function() {\n}\n\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\t\t\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-gateway-config-single-device-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gatewayTitle\":\"Gateway configuration (Single device)\"},\"title\":\"Gateway configuration (Single device)\"}" - } - } + "widgetTypeFqns": [ + "gateway_widgets.gateway_configuration", + "gateway_widgets.attributes_card", + "gateway_widgets.config_form_latest" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/gpio_widgets.json b/application/src/main/data/json/system/widget_bundles/gpio_widgets.json index e539824aca..ffe5d3449a 100644 --- a/application/src/main/data/json/system/widget_bundles/gpio_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/gpio_widgets.json @@ -3,84 +3,14 @@ "alias": "gpio_widgets", "title": "GPIO widgets", "image": "data:image/png;base64,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", - "description": "Visualization and control of GPIO state for target devices." + "description": "Visualization and control of GPIO state for target devices.", + "externalId": null, + "name": "GPIO widgets" }, - "widgetTypes": [ - { - "alias": "basic_gpio_control", - "name": "Basic GPIO Control", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAND0lEQVR42u2cCVBVZRvHqcmpGbdSZ+SCgIYhmhskiBqgCMqSGoaiDIvgAiiCmpk6NpbVpI5WblOpn7h8bohSosiOCMiOuI5rmvip+bmh4wYU31/ezzOny13OhQuN9P/NO845x3PPgfv+eLfzPMektra2urr61q1bV69evUJII4BCEKmqqgpSmcCqioqKysrKmpqaWkIaARSCSNAJUplAMezwSyHGAjpBKhM0X2yriHHbLUhlgq6R3wUxLpCKYhGKRSgWoVgUi1AsQrEIxSKEYhGKRSgWIRSLUCxCsQihWIRiEYpFCMUiFItQLEIoFqFYhGIRQrEIxSIUixCKRSgWoViEUCxCsQjFIoRiEYpFKBYhFItQLEKxCKFYhGIRikUIxSIUi1AsQigWoViEYhFCsQjFIhSLEIpFKBahWM3BEVfXQ126sCgpN5OSKBbFolgUi2JRLIpFsZpPrOSuXTMHDMh2ccl2ds6wt0+2smrSCk7p3h23y3JyyrCzw60pVgsUC3WcN3r0sZkzy2fNkkrZjBm5Xl5NUeWwtjgoSH6vY9HRBX5+aT17UqyWI1Z6796l4eHyapaX4rCwJBubX8zNNZZEc/MkgxpFC4tcb29t94LZ6YMGab1Xly4HzM3/uWJVVVU17OrPnj1rfrFSbWxKIyK01bQoBZMnb7Gw+JepqcayydR0u0qVqKzKcz09dd+rPCYm3s5O271QNqtUexunlw6xmqju/vjjj5qamgaK9eeff3711VdWVladOnWysLD46aefxPH58+e/VUfHjh3t7Ow2bNggjltaWh4/flx8cMmSJR06dHj99df79OmTnp5e/8fKyMgICgqaM2eOQWIlW1oW+fv/Jz6+wNdX27dcMG6cnpquK6ne3joqGyXW1PQXffWd3qePknuVRERs1u6xKDvNzDTeoiQ4+E5+/t3CwgvffmuQWEeOHHFwcEAdoSLwVT948AAHS0tL33qBtbV1eHj43bt3cfzjjz9etGiR+ODhw4f79u2LusM5ixcvRmXJL1tZWTlhwgQzM7POnTtPnDjxyZMnBov1448/vv322+fPn8f22bNncSGhSExMTHR0tPCjqKhIpVL9/PPP2G3Xrl1ZWRk2li9f3rt370uXLuGEffv24fi5c+fkVx4/fvzQoUPd3NwiIiIMEutYZOTlDRtu5+YWBwRo/IrTevRQUtNiDKS3smNVqoM6xVIoMUqii4vue6HUb7dSunV7dOUKBmqYdtwrK8v18FAo1vXr19u3b79161b8kT969MjPzy80NBTHCwsL4Zk45/fff//oo4/GjRuH7cjISLiFjYsXL6K+9uzZgwbp8uXL/fv3//rrr+VXnjVrVkBAQHV1NZRycnL67rvvDBbL0dFx48aN0u727dt3794tF0uAH2j69OmSWPhNzM3NU1NTpRPCwsJmz54tv7L46/nss88MFUuUa3Fx2sQ6MnSowppG2efg0IDKljefapMDHSVn4kS999qqUtUXC7+R2MafU5aDg0KxVq1aNWzYMGkXughv5GIB1Bf8k4v1ySefhISESCegYzE1NZU3Wmg1RPMhJNPW5+gSCz9BSUlJ/eNqYnl6en755ZeSWDdv3jQxMUGDKW/50DjVv05TiJX3wQfKxUoaPlxvZWvroZ4P5mxtld+rODxc771QtM0byqZOxW+tvCucOXMmar3+t6om1rZt22xtbeVieXh4/PDDD9IJDx8+RG2i/at/qcePH9vY2KDDNVisV155RXRhBw8enFaHGE5BLPR02EVTZG9vj2GW6KeFWBcuXHj11VfRbknX2bVrFzr75hHr6OjRBojl7t4YsdBDKb9XyfTpSsTS2PMe9fG5W1SESYlysQIDA/H1YuP27dvTXoBKgVgYPIndUaNGYfSMypWLhd5tx44d8ku1atVKbSQjmDx5MkxoyOAd477s7GxsnDlzJi4uDkM2DNaEWF5eXjiSkJCAwaDUTgqx7t27B8fRf8ubZbRqzSNWjru78sr+ZfBgvTUdp10srIcpv1deSIjee22u1xWiZA8Zcq+4ONPOzqBZIepo6tSp2MAACzW1bt06VApqCmK1adMmrg5U7v3798X5klg+Pj7yYRO8xAfxr9r10Ue5uro+ffq0IWL5+vqKmwkwQ5TEkneFEtLgHTPBTZs2Scfxs+KzzSNWer9+yit7m7W17prGuoPudaaSSZMU3kvvJFSbxI9+++3G/v2X1q1DKZsyRaFY8fHxmKRLSwbPq/mFWPKuUEISa+nSpcOHD5eOo/Xq2bOn2skYeaOnkqQ0WKwTJ05gwonB2unTpzMzMwcOHCi6bb1iHThwAK1dbGws5ozz5s3DUoXoK40l1mEnpzRbW23LlcXKKjvb319vTe/R3lz9f67g4qJQrB09eui+1xYtM9A8Ly+pZA0cqFAszOmcnZ3HjBlTUFBQXl6OITkm9aIr1C0WdOnWrVtUVBQ+iOkaPrV37175mYmJiahcdKAlL2jIAimUmjRpEpQaOXLk999/L/4CYIy8QZIIDg7+9ddfxTZEHDt2LDps/MQVFRUaL46x1/r1642+QJrRvz/WJHVXc2lU1HYbG92LWHv1WSUmhsUhIXqtSvPx0W3Vdn3rGg1YIEUn+MUXX7i4uAwZMgRjeVHLGAGLdQc1MHresmWL2L5x4wbOHzRo0IcffpicnKx2JjQY91fk4+kW/kjn8Pvvl0dH63jMkjRgAFojjSXe3DzBkKc6GFOXTZumw6r8gID4Ll203W5v3UMkPit8aR5CZ9rbl4SFaZj2h4TgSaKRgxqsrQv9/TU+zMFjxEOWlnwI3bLCZiwtM997L8/bG+vj+X5+GKNk6JxbNbKkv/suNIJhRcHBuF2OhwciLBg2w0A/BvpRLBaKRbEoFsWiWBSLhWJRLIpFsSgWxWKhWH+jWHich6cuqe+8c8jCgmJRLCOIleXoWBQQcOzFQ0M8Iizw92/SxXeK1cLFQqh44fjx2p4KH8VD+6Z/fkexWppYz1PEAgN1x7Ec9vNDuKbGgriobSpVvJlZEsVqOrHy8xH0EYBUiClTppw6dUocRDyWCMRBQOmCBQtEfljtX+Ox8vLy8L/u7u5z586VhykL7ty5g7Ay/C/ippElplwsRAOfWby4Yteuk3Pnahsz6c8grSv7nZ315swYJU2ZYqmTk5OD3KC1a9ciSnD16tWIPBQOIYIUIWBpaWmIAkPsMwJHEWtaK4sgTUlJwcmIs0YeIoL2kZwoT9oBiD5D2hCiCFesWNG9e3e1lEgdYl1YseLS2rUIpLyekFA+Y4bGTlBhSlZxRMQmMzM9bpmaUizji4UMiM8//1zahQorV66srReajMZMRLVKYiEgWh4aipZp2bJl0i7CUGEbkofE7htvvIHESIVi5Y4YIYKST3366blvvtEQ4jd4sPKY9z39+umNTt6nII6UYhkmFvJOc3Nz6x/XLRaCphG3j+xC6QRkfXh7e2u8Bdo2RMRrTPbQNsbKcXO7uHr1nYIChPJpME/7+znqlwOurvrTvzRlzlCsRomF9ECRUCbSvwAC7Gtl6V87d+5cuHChWleIpFu1vEKchqRqtYsjVRL5/6+99po8Z1qJWM9DQ4OC/puVVRoaqiEFb8wY5WIljxihV6x/Uyyji9W6dWuk2dS+SFhF0qmU/iUSVpEogcR+acAuxEIoPloskUQvQLcoT/eWQG+IfArkgWjM4dEoFtKtxBunkP51bffuBo/clb9PYSe7QqOLhTyNNWvWSLsK8woxEke2v/wNM3ilCbI+pF1kbeNNIdIu0tbwggCFYl1cteq32Nj8UaNuJCYej4nR0J45OioXa2evXnrFSngZJoYvmVjo7PAmGfFmIrxmKbCOWgV5hVAQefci4R95YJhanjx5Ut5QIRkXCxnYxrD9zTffvHbtmtLlBiur8qgo6FUSEqJ5EcvKqiwyUolVeEVWy+gHX8p1LLwTC80PxteofoyrxPRNr1jIlsT6Vtu2bbHo0LVr1/3796udiSM4jrYK2bp41Y5xF0gVJpHGDxigN9cviQukTbryjoleA97Nhz5RbflKDfk4zLiPdPJ9fXVble7pidZIW9mFN369JEujfKTTrM8Kn78XFKN4TTmrWD494ubGh9AUq+ElrVevPB8fpK1CJpTi0FC8MjlF+8uAKBbFYqFYFItiUSyKRbFYKFaDuLRmDUKvWJSUB2fOUCzyj4BiEYpFKBahWBSLUCxCsQjFoliEYhGKRSgWIRSLUCxCsQihWIRiEYpFCMUiFItQLEIoFqFYhGIRQrEIxSIUixCKRSgWoViEUCxCsQjFIoRiEYpFKBYhFItQLEKxCKFYhGIRikUIxSIUi1AsQigWoViEYhFCsQjFIhSLEIpF/l6xrl69WlNTw++CGAvoBKlMbt26VVlZya+DGIv79+9DKpOqqqqKigq4xXaLNL6tgkjQCRsm2K+uroZiaL6uENIIoBBEEi3U/wDz+OXgAWa1/wAAAABJRU5ErkJggg==", - "description": "Allows to change state of the GPIO for target device using RPC commands. Requires handling of the RPC commands in the device firmware. Uses 'getGpioStatus' and 'setGpioStatus' RPC calls", - "descriptor": { - "type": "rpc", - "sizeX": 4, - "sizeY": 2, - "resources": [], - "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n {{rpcErrorText}}\n \n
", - "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel mat-slide-toggle {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel.col-0 mat-slide-toggle {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 mat-slide-toggle {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", - "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-control-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n \n var i, gpio;\n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n scope.gpioList = [];\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false\n }\n );\n }\n\n scope.requestTimeout = settings.requestTimeout || 1000;\n\n scope.switchPanelBackgroundColor = settings.switchPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioStatusRequest = {\n method: \"getGpioStatus\",\n paramsBody: \"{}\"\n };\n \n if (settings.gpioStatusRequest) {\n scope.gpioStatusRequest.method = settings.gpioStatusRequest.method || scope.gpioStatusRequest.method;\n scope.gpioStatusRequest.paramsBody = settings.gpioStatusRequest.paramsBody || scope.gpioStatusRequest.paramsBody;\n }\n \n scope.gpioStatusChangeRequest = {\n method: \"setGpioStatus\",\n paramsBody: \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n };\n \n if (settings.gpioStatusChangeRequest) {\n scope.gpioStatusChangeRequest.method = settings.gpioStatusChangeRequest.method || scope.gpioStatusChangeRequest.method;\n scope.gpioStatusChangeRequest.paramsBody = settings.gpioStatusChangeRequest.paramsBody || scope.gpioStatusChangeRequest.paramsBody;\n }\n \n scope.parseGpioStatusFunction = \"return body[pin] === true;\";\n \n if (settings.parseGpioStatusFunction && settings.parseGpioStatusFunction.length > 0) {\n scope.parseGpioStatusFunction = settings.parseGpioStatusFunction;\n }\n \n scope.parseGpioStatusFunction = new Function(\"body, pin\", scope.parseGpioStatusFunction);\n \n function requestGpioStatus() {\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusRequest.method, \n scope.gpioStatusRequest.paramsBody, \n scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n for (var g = 0; g < scope.gpioList.length; g++) {\n var gpio = scope.gpioList[g];\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled; \n self.ctx.detectChanges();\n }\n }\n );\n }\n \n function changeGpioStatus(gpio) {\n var pin = gpio.pin + '';\n var enabled = !gpio.enabled;\n enabled = enabled === true ? 'true' : 'false';\n var paramsBody = scope.gpioStatusChangeRequest.paramsBody;\n var requestBody = JSON.parse(paramsBody.replace(\"\\\"{$pin}\\\"\", pin).replace(\"\\\"{$enabled}\\\"\", enabled));\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusChangeRequest.method, \n requestBody, scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled;\n self.ctx.detectChanges();\n }\n );\n }\n \n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n scope.gpioClick = function($event, gpio) {\n if (scope.rpcEnabled && !scope.executingRpcRequest) {\n changeGpioStatus(gpio);\n }\n };\n \n scope.gpioToggleChange = function($event, gpio) {\n gpio.enabled = !$event.checked;\n $event.source.toggle();\n self.ctx.detectChanges();\n }\n \n if (scope.rpcEnabled) {\n requestGpioStatus(); \n }\n \n self.onResize();\n}\n\nself.onResize = function() {\n var scope = self.ctx.$scope;\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.prefferedRowHeight = prefferedRowHeight;\n var ratio = prefferedRowHeight/32;\n \n var css = '.mat-slide-toggle .mat-slide-toggle-bar {\\n' +\n ' height: ' + 14*ratio+'px;\\n'+\n ' width: ' + 36*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb-container {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-ripple {\\n' +\n ' height: ' + 40*ratio+'px;\\n'+\n ' width: ' + 40*ratio+'px;\\n'+\n ' top: calc(50% - '+20*ratio+'px);\\n'+\n ' left: calc(50% - '+20*ratio+'px);\\n'+\n '}\\n';\n css += '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n\n cssParser.createStyleElement(namespace, css);\n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-gpio-control-widget-settings", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"Basic GPIO Control\"}" - } - }, - { - "alias": "gpio_panel", - "name": "Basic GPIO Panel", - "image": "data:image/png;base64,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", - "description": "Allows to display state of the GPIO for target device using latest attribute values. You should set the label of the selected data key to GPIO pin number (e.g. '1') and use boolean values for widget to display the data.", - "descriptor": { - "type": "latest", - "sizeX": 5, - "sizeY": 2, - "resources": [], - "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n
", - "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", - "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-panel-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.detectChanges();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var css = '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n \n cssParser.createStyleElement(namespace, css); \n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-gpio-panel-widget-settings", - "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"color\":\"#008000\",\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"color\":\"#ffff00\",\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"color\":\"#cf006f\",\"_uniqueKey\":2}],\"ledPanelBackgroundColor\":\"#b71c1c\"},\"title\":\"Basic GPIO Panel\",\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"1\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.22518255793320163,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"2\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7008206860666621,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"3\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.42600325102193426,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 1000;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}" - } - }, - { - "alias": "raspberry_pi_gpio_panel", - "name": "Raspberry Pi GPIO Panel", - "image": "data:image/png;base64,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", - "description": "Allows to change state of the GPIO for Raspberry Pi device using RPC commands. Requires handling of the RPC commands in the device firmware. Uses 'getGpioStatus' and 'setGpioStatus' RPC calls", - "descriptor": { - "type": "latest", - "sizeX": 7, - "sizeY": 10.5, - "resources": [], - "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n
", - "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", - "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-panel-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.detectChanges();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var css = '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n \n cssParser.createStyleElement(namespace, css); \n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-gpio-panel-widget-settings", - "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"gpioList\":[{\"pin\":1,\"label\":\"3.3V\",\"row\":0,\"col\":0,\"color\":\"#fc9700\",\"_uniqueKey\":0},{\"pin\":2,\"label\":\"5V\",\"row\":0,\"col\":1,\"color\":\"#fb0000\",\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 2 (I2C1_SDA)\",\"row\":1,\"col\":0,\"color\":\"#02fefb\",\"_uniqueKey\":2},{\"color\":\"#fb0000\",\"pin\":4,\"label\":\"5V\",\"row\":1,\"col\":1},{\"color\":\"#02fefb\",\"pin\":5,\"label\":\"GPIO 3 (I2C1_SCL)\",\"row\":2,\"col\":0},{\"color\":\"#000000\",\"pin\":6,\"label\":\"GND\",\"row\":2,\"col\":1},{\"color\":\"#00fd00\",\"pin\":7,\"label\":\"GPIO 4 (GPCLK0)\",\"row\":3,\"col\":0},{\"color\":\"#fdfb00\",\"pin\":8,\"label\":\"GPIO 14 (UART_TXD)\",\"row\":3,\"col\":1},{\"color\":\"#000000\",\"pin\":9,\"label\":\"GND\",\"row\":4,\"col\":0},{\"color\":\"#fdfb00\",\"pin\":10,\"label\":\"GPIO 15 (UART_RXD)\",\"row\":4,\"col\":1},{\"color\":\"#00fd00\",\"pin\":11,\"label\":\"GPIO 17\",\"row\":5,\"col\":0},{\"color\":\"#00fd00\",\"pin\":12,\"label\":\"GPIO 18\",\"row\":5,\"col\":1},{\"color\":\"#00fd00\",\"pin\":13,\"label\":\"GPIO 27\",\"row\":6,\"col\":0},{\"color\":\"#000000\",\"pin\":14,\"label\":\"GND\",\"row\":6,\"col\":1},{\"color\":\"#00fd00\",\"pin\":15,\"label\":\"GPIO 22\",\"row\":7,\"col\":0},{\"color\":\"#00fd00\",\"pin\":16,\"label\":\"GPIO 23\",\"row\":7,\"col\":1},{\"color\":\"#fc9700\",\"pin\":17,\"label\":\"3.3V\",\"row\":8,\"col\":0},{\"color\":\"#00fd00\",\"pin\":18,\"label\":\"GPIO 24\",\"row\":8,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":19,\"label\":\"GPIO 10 (SPI_MOSI)\",\"row\":9,\"col\":0},{\"color\":\"#000000\",\"pin\":20,\"label\":\"GND\",\"row\":9,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":21,\"label\":\"GPIO 9 (SPI_MISO)\",\"row\":10,\"col\":0},{\"color\":\"#00fd00\",\"pin\":22,\"label\":\"GPIO 25\",\"row\":10,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":23,\"label\":\"GPIO 11 (SPI_SCLK)\",\"row\":11,\"col\":0},{\"color\":\"#fd01fd\",\"pin\":24,\"label\":\"GPIO 8 (SPI_CE0)\",\"row\":11,\"col\":1},{\"color\":\"#000000\",\"pin\":25,\"label\":\"GND\",\"row\":12,\"col\":0},{\"color\":\"#fd01fd\",\"pin\":26,\"label\":\"GPIO 7 (SPI_CE1)\",\"row\":12,\"col\":1},{\"color\":\"#ffffff\",\"pin\":27,\"label\":\"ID_SD\",\"row\":13,\"col\":0},{\"color\":\"#ffffff\",\"pin\":28,\"label\":\"ID_SC\",\"row\":13,\"col\":1},{\"color\":\"#00fd00\",\"pin\":29,\"label\":\"GPIO 5\",\"row\":14,\"col\":0},{\"color\":\"#000000\",\"pin\":30,\"label\":\"GND\",\"row\":14,\"col\":1},{\"color\":\"#00fd00\",\"pin\":31,\"label\":\"GPIO 6\",\"row\":15,\"col\":0},{\"color\":\"#00fd00\",\"pin\":32,\"label\":\"GPIO 12\",\"row\":15,\"col\":1},{\"color\":\"#00fd00\",\"pin\":33,\"label\":\"GPIO 13\",\"row\":16,\"col\":0},{\"color\":\"#000000\",\"pin\":34,\"label\":\"GND\",\"row\":16,\"col\":1},{\"color\":\"#00fd00\",\"pin\":35,\"label\":\"GPIO 19\",\"row\":17,\"col\":0},{\"color\":\"#00fd00\",\"pin\":36,\"label\":\"GPIO 16\",\"row\":17,\"col\":1},{\"color\":\"#00fd00\",\"pin\":37,\"label\":\"GPIO 26\",\"row\":18,\"col\":0},{\"color\":\"#00fd00\",\"pin\":38,\"label\":\"GPIO 20\",\"row\":18,\"col\":1},{\"color\":\"#000000\",\"pin\":39,\"label\":\"GND\",\"row\":19,\"col\":0},{\"color\":\"#00fd00\",\"pin\":40,\"label\":\"GPIO 21\",\"row\":19,\"col\":1}],\"ledPanelBackgroundColor\":\"#008a00\"},\"title\":\"Raspberry Pi GPIO Panel\",\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"7\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.22518255793320163,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"11\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7008206860666621,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"12\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.42600325102193426,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"13\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.48362241571415243,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"29\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7217670147518815,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}" - } - }, - { - "alias": "raspberry_pi_gpio_control", - "name": "Raspberry Pi GPIO Control", - "image": "data:image/png;base64,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", - "description": "Allows to display state of the GPIO for target Raspberry Pi device using latest attribute values. You should set the label of the selected data key to GPIO pin number (e.g. '1') and use boolean values for widget to display the data.", - "descriptor": { - "type": "rpc", - "sizeX": 6, - "sizeY": 10.5, - "resources": [], - "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n {{rpcErrorText}}\n \n
", - "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel mat-slide-toggle {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel.col-0 mat-slide-toggle {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 mat-slide-toggle {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", - "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-control-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n \n var i, gpio;\n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n scope.gpioList = [];\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false\n }\n );\n }\n\n scope.requestTimeout = settings.requestTimeout || 1000;\n\n scope.switchPanelBackgroundColor = settings.switchPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioStatusRequest = {\n method: \"getGpioStatus\",\n paramsBody: \"{}\"\n };\n \n if (settings.gpioStatusRequest) {\n scope.gpioStatusRequest.method = settings.gpioStatusRequest.method || scope.gpioStatusRequest.method;\n scope.gpioStatusRequest.paramsBody = settings.gpioStatusRequest.paramsBody || scope.gpioStatusRequest.paramsBody;\n }\n \n scope.gpioStatusChangeRequest = {\n method: \"setGpioStatus\",\n paramsBody: \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n };\n \n if (settings.gpioStatusChangeRequest) {\n scope.gpioStatusChangeRequest.method = settings.gpioStatusChangeRequest.method || scope.gpioStatusChangeRequest.method;\n scope.gpioStatusChangeRequest.paramsBody = settings.gpioStatusChangeRequest.paramsBody || scope.gpioStatusChangeRequest.paramsBody;\n }\n \n scope.parseGpioStatusFunction = \"return body[pin] === true;\";\n \n if (settings.parseGpioStatusFunction && settings.parseGpioStatusFunction.length > 0) {\n scope.parseGpioStatusFunction = settings.parseGpioStatusFunction;\n }\n \n scope.parseGpioStatusFunction = new Function(\"body, pin\", scope.parseGpioStatusFunction);\n \n function requestGpioStatus() {\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusRequest.method, \n scope.gpioStatusRequest.paramsBody, \n scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n for (var g = 0; g < scope.gpioList.length; g++) {\n var gpio = scope.gpioList[g];\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled; \n self.ctx.detectChanges();\n }\n }\n );\n }\n \n function changeGpioStatus(gpio) {\n var pin = gpio.pin + '';\n var enabled = !gpio.enabled;\n enabled = enabled === true ? 'true' : 'false';\n var paramsBody = scope.gpioStatusChangeRequest.paramsBody;\n var requestBody = JSON.parse(paramsBody.replace(\"\\\"{$pin}\\\"\", pin).replace(\"\\\"{$enabled}\\\"\", enabled));\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusChangeRequest.method, \n requestBody, scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled;\n self.ctx.detectChanges();\n }\n );\n }\n \n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n scope.gpioClick = function($event, gpio) {\n if (scope.rpcEnabled && !scope.executingRpcRequest) {\n changeGpioStatus(gpio);\n }\n };\n \n scope.gpioToggleChange = function($event, gpio) {\n gpio.enabled = !$event.checked;\n $event.source.toggle();\n self.ctx.detectChanges();\n }\n \n if (scope.rpcEnabled) {\n requestGpioStatus(); \n }\n \n self.onResize();\n}\n\nself.onResize = function() {\n var scope = self.ctx.$scope;\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.prefferedRowHeight = prefferedRowHeight;\n var ratio = prefferedRowHeight/32;\n \n var css = '.mat-slide-toggle .mat-slide-toggle-bar {\\n' +\n ' height: ' + 14*ratio+'px;\\n'+\n ' width: ' + 36*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb-container {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-ripple {\\n' +\n ' height: ' + 40*ratio+'px;\\n'+\n ' width: ' + 40*ratio+'px;\\n'+\n ' top: calc(50% - '+20*ratio+'px);\\n'+\n ' left: calc(50% - '+20*ratio+'px);\\n'+\n '}\\n';\n css += '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n\n cssParser.createStyleElement(namespace, css);\n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-gpio-control-widget-settings", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#008a00\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":7,\"label\":\"GPIO 4 (GPCLK0)\",\"row\":3,\"col\":0,\"_uniqueKey\":0},{\"pin\":11,\"label\":\"GPIO 17\",\"row\":5,\"col\":0,\"_uniqueKey\":1},{\"pin\":12,\"label\":\"GPIO 18\",\"row\":5,\"col\":1,\"_uniqueKey\":2},{\"_uniqueKey\":3,\"pin\":13,\"label\":\"GPIO 27\",\"row\":6,\"col\":0},{\"_uniqueKey\":4,\"pin\":15,\"label\":\"GPIO 22\",\"row\":7,\"col\":0},{\"_uniqueKey\":5,\"pin\":16,\"label\":\"GPIO 23\",\"row\":7,\"col\":1},{\"_uniqueKey\":6,\"pin\":18,\"label\":\"GPIO 24\",\"row\":8,\"col\":1},{\"_uniqueKey\":7,\"pin\":22,\"label\":\"GPIO 25\",\"row\":10,\"col\":1},{\"_uniqueKey\":8,\"pin\":29,\"label\":\"GPIO 5\",\"row\":14,\"col\":0},{\"_uniqueKey\":9,\"pin\":31,\"label\":\"GPIO 6\",\"row\":15,\"col\":0},{\"_uniqueKey\":10,\"pin\":32,\"label\":\"GPIO 12\",\"row\":15,\"col\":1},{\"_uniqueKey\":11,\"pin\":33,\"label\":\"GPIO 13\",\"row\":16,\"col\":0},{\"_uniqueKey\":12,\"pin\":35,\"label\":\"GPIO 19\",\"row\":17,\"col\":0},{\"_uniqueKey\":13,\"pin\":36,\"label\":\"GPIO 16\",\"row\":17,\"col\":1},{\"_uniqueKey\":14,\"pin\":37,\"label\":\"GPIO 26\",\"row\":18,\"col\":0},{\"_uniqueKey\":15,\"pin\":38,\"label\":\"GPIO 20\",\"row\":18,\"col\":1},{\"_uniqueKey\":16,\"pin\":40,\"label\":\"GPIO 21\",\"row\":19,\"col\":1}]},\"title\":\"Raspberry Pi GPIO Control\"}" - } - } + "widgetTypeFqns": [ + "gpio_widgets.basic_gpio_control", + "gpio_widgets.gpio_panel", + "gpio_widgets.raspberry_pi_gpio_panel", + "gpio_widgets.raspberry_pi_gpio_control" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index 2d382b5958..5248b73639 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -2,106 +2,16 @@ "widgetsBundle": { "alias": "home_page_widgets", "title": "Home page widgets", - "image": "data:image/png;base64,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", + "image": "data:image/png;base64,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", "description": "Contains useful widgets to design the user's home page.", "externalId": null, "name": "Home page widgets" }, - "widgetTypes": [ - { - "alias": "getting_started", - "name": "Getting started", - "image": "data:image/png;base64,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", - "description": "Getting started instruction.", - "descriptor": { - "type": "static", - "sizeX": 7.5, - "sizeY": 6.5, - "resources": [], - "templateHtml": "\n\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "", - "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Getting started\",\"dropShadow\":true}" - } - }, - { - "alias": "documentation_links", - "name": "Documentation links", - "image": "data:image/png;base64,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", - "description": "Configure and display links to documentation and any other resources.", - "descriptor": { - "type": "static", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "\n\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-doc-links-widget-settings", - "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Documentation links\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" - } - }, - { - "alias": "usage_info", - "name": "Usage info", - "image": "data:image/png;base64,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", - "description": "Display statistics about platform entities and API usage.", - "descriptor": { - "type": "static", - "sizeX": 7.5, - "sizeY": 6.5, - "resources": [], - "templateHtml": "\n\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "", - "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Usage info\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" - } - }, - { - "alias": "quick_links", - "name": "Quick links", - "image": "data:image/png;base64,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", - "description": "Configure and display quick links to the platform's UI components.", - "descriptor": { - "type": "static", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "\n\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-quick-links-widget-settings", - "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Quick links\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" - } - }, - { - "alias": "dashboards", - "name": "Dashboards", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAFACAMAAAClX9rRAAABU1BMVEUAAADh4eHg4ODf39/g4ODh4eHi4uLj4+P////w8PDg4OAwVoD1+fk9PT2enp62trbNzc3n5+eqqqrz8/NVVVWGhobCwsJ1dXXl5eXZ2dmru8xJSUnPz89tbW1ubm764gX5+fmFhYXr6+thYWGSkpLu7u7U1NSkpKRgfZz39/dgfJyxsbGRkZFJao/y8vKXl5fIyMjk5OR5eXnb29u8vLyVlZX8/Pz9/f3c5epWVlbx8fHt7e3E0NvLy8t5k619fX1iYmKSp7zo7/J6enr09fegoKC6urpjgJ88YIdVdJefscSYqr+GnbV9la+rq6uyv863xtNuiabY3uSZmZk9YIfl6u/Q2uK/ytbT09P+++CmtseQo7jDw8M8YIj75iT++tH65BX9+L+EmbF4kKpshqP77Frk5unL1N+Kn7f76UP88pj88IH+/e+hoaE9YIj75zM9YYhav5TlAAAACHRSTlMAj88Q358/HxRJgpIAAB91SURBVHja7NG7CoNAGETh3fX2M6VFthVhA5JikYCd5P3fKpogpDAgaRzIfNXpj1vVvoScLFSNeys8hIIvXj8CkAaTk3U9ENYjHuNkQmAa4Z1rAP0gMQG1q9CbkEjwLqAzITGgdIAJDUBDqGgIGQ0hoyFkNISMhpDREDIaQkZDyGgImf0haTHYV/ect8z5agdd8s3ktyE5Lh5tsn1zjFvGONtBbcy2K+FTsr/2ZNeOWVyHYTiAr5pzKH+PjwMfCAfMkdEYTfFn6HBfod9/fZVy5l1pm77p6GBBqZBkD/k1MaV9BKLMAOoA+b04BslEZQPCb4D8eevxOUAOQEjU39Yzc14dIvMyRQeJl7Q6yFfiJYm1m7d/DPoRkzlQvMw0B+lDd+N93CGHIJSgRDPACnwQNWBRaDAQxf5Eg4ePT56x+CD7IPngKtynbMtFwfLA4/1zgByAVCASI30XgJlkwWT1JoWxWY2LAaxWrFQUEwkwe+YgrZZq/ZMiUwCCtb4eeNAAeQoSo3/mmQhGE9Zgdb9/uJ8hikTZfGiG0r5kckO/9N7yQvBCWMsDjwHyHITmzKxgU4Geq9AO0pGqz0+0f+xXWyLNl+TeZrQdxCbB50oPPAbIMcgE9ad+ahlsQBkAx6cgbEuWeyB9D7nrMUCOQSJjogCs+8WXGIlWxnYLsiDtZw2dALm8il//n+29IFFIqqLd8xggxyBhgZZvkA1MK3Q/Gq5BJqIIfFCDRiLe20LC/0CStbwwg337dMdjgByBeGglEgWfF0BJGNwSUK9BwFktE/YMhQrAkwJLBym6t7LttrQNqLceA+QZCKdieWCAZ0CoTIY00zXIzEC2ybIB8G+QJyslqDhIL/gzrGTf49ZjgFyBHEaMPZMYD/re7qX/3+PNPAbIC/0e8j7+W/xaICMGyOvGAHmxGCAvFgPkLzvm1po4FATg14wTUBAk5qn4sOqWs1ho1oMEISHBJFrvWnHxpS1tn5b9/087k9RVd8V7Ul38aJNzS1vO15mJ58y4CjkzrkLOjKuQM+Mq5My4CjkzrkLOjKuQM+Mq5My4CjkzrkKOw2kKPBjRdK5CTovj41H4zt5CqmXnWw0um1rWKX9X4uAdj6S5p5CbLPwvZG+U0yPwSMReQr5zaLxOn55Lp+blR+cVkqZ2eiV4NHsIqZYBoFNCvxXop8bq+fj8CEnjVC9YSPUbwKPwLFuNB1kXL31ImFr1YoVUawBPnqXGiGxhEzZz7kYSE8I+XktCqvFiih+wmTM3kpyQMsCLb6txI5M3kr1IITcAP4Stxo/0ppAwN5copAaPnlSToOttrexnnLQSEsIBIiw1GXrPkDDlyxOShalYSivm7slL13cr5q3FT8etIVKYZPKwO7f3W0IkdiENf9Gejj4676PeoUKqAKWlja2jqa5H6mN1FcSdhGB9KUS21fXKYOAargagZQqwA+00bOZ7zEJ6ijPEOR2lMT+2ejhUyA30Ue4iREfr75Hu3kJMDzaiGTmAu6JbgLyhnURIOWYhj4rSO6mQLHR8dZ0QO7CiDTeDgIx169iL+l0OqDHFSzfsm5Yu6arbNGPSBPdsff6s1V0RYm8p6xmjAEAy8pOcUclzs1LR+DbRKinIaPkcd3IZLbRHc1uFZOMVIpQ35w2Z4UNTsBBuNI4S8tRaJ8RC9JA/nTToTsFR4t++WMIXQX27weNj1aK+xAZPjFUpeJCebfEfSkIW+FPYAKnIQMh90SjeA7QHt22jApCmTFYBozhwud12Oal9MQbtgbtNyLd4hTwoD28K5ywxoiUOC+lTY3S4kBq8WGuE2LyhLQzULu2n9NBepCwTyaAnVBZCSyxben44qCMt83kCTTJk0aCQsr4ipL6liKSN2wkHSZSyJsYdDbk8nAcgITSV4bF2G8C9BbgbpGEL8QrpO8Oe0gyzVfOnT0JI0eNP0T9cCNf0dREibUpaaLEQqdpyqYbYnoiksBBvFj4kVa+h9nzUbaxHVYOVtVAnf3sJgUzbMNJaJCQiZ4RSiDB8imlgK7QgtUsNgWqcQnylg7+cEbUc56OGvCkCsXl6IWYDCStMO36wJIRHZAvHoRAbI0y1N1M9vdQzUaeVITNOX3sKYe4ybnEupOIaxIoQY+C67sDQqNx8vpCO8i7EIycqpf8hZKQcV9Rr8BysESKHwpRB6GAclFAsCzEx4MQUCRE6I9UAA5TBzEJJK3s6sxCy83tvKgVRUY+EZIxJ4e8IGaTzDM1pLORzU9YompoiKqOTCXmqrxHS5e1nIXrdpm3E8VwI4/kYqPOUJVkffeHMp8vQJ4PY4MwWPv+3kFIHNnFbDLUYk0hImrqrQqLyES3K7yKkFqMQrhcPhOMMccSlnWKFv48S4sDUXxFSt4ixiUIPhiwEe2ZXeFzU57mNs5j8EBKgCPSG4B62Pi5qCet6IFpUbWaBLlaE4NuWt6x0qpAvune03184ODLal8GqkImRKxTu2+TCTRVyxie+9kYCiKnyTgr6D02F+g1l9N50DhdShle0l4WE6OFrb53/1S0PUVDY2ALxT85ihyyEpgVPh5bM6EJL+aeUyFlXoNdaFmIK2Ey+SDWjneLibbShkDYM997QloRwXYlW3PG9+JkfDAXlKYIcvLEVZdSJokNxpocLqQJ8tdQ12HLuSco/L17rkHLtw2ufqT/DNgpaAULozj34F01buid3dLKFn2LROO5wsTNTE0Ji4ufvSsQFnfYmePzOAZIwN5cn5Df7Zq+CMAyF0bWCW7ZOXQ1cRxUHQclQrSBIQFC61CK+/xOYjE4lzQ837Xde4XCnez77oGrTPKhKyvhA0gnZmOQkzQuX8MKdeeSgiiyFmEzu08fPgDQyIJdQ7tVelhE5XWuEckNC/o08iMpYV7I7aomU1Dm2fso4sfWBapk+thY5x9aGys4R7reul6F5d5gjOAuxVOvFVBCrbREc7euDRkzaRP6Ttn2sSVvjK6TB6DMoivx8nBWEhEV9MYueNhDCDAhhBoQwA0KYASHMgBBmQMiPvTpWcRwGAjDc/rjQTEYg5G5IIReCYIg6v/97HTqHPQ6W3SpBGP1g8Lia4Ss8WBNksCbIYE2QwZoggzVBBmuCDNYEGawJMlgTZLAmyMc7jgkyVCYTZKjeCZILgDpX7tfzlpUfsvxBEA0ZCJEr99157T4oiJmfG9/KAjy9rMT+2r9cxOkF4qVEXoc+634A8e/NHDfwZ4n0+Yjw9HOGmx6fBUltp2+sdW0CoalVUVNIrdg1fk4nSDRNm7OaNkEtZcA3ZxE0ES2pRd+g7hxGtH03R01T+Afi+gXi+iYQ33LfeHkQA4Q7MTxYjJvRd71CJ8g9Q11pBVeQTE8Kae0ga4J9R3Ks1oe9z4pl6M8rDekF4jUs7wFhMQ+RmKp0kNhduoaaSDWu0AmCVtlW7iYlfoFkYXugiWYillBdi+S2ILXPHvgPhNRFTLqH8iYQ5A875rLiNgyF4e1PQRdLtrBx4lyxCS4hTLIx3QTyFLNpO4tZ9AJ9/13PcdwUN3FCaSdVB33gSLJu9vmcE+KNtDAlbE9IuVsTeA0chZS7FVTGEjgXdUJQvdm1UXCa7nZFgtS81NS/K7k9ldNWSN+IUa2PFxOyrqRFlWLbE5JWVH1NvyHOwZoMdQoOs+pubVNRhVNWPcW7OWAMdStA0xFZTmmWQtM3Ymr28XJCUEpLh1FU/BQCXSmT4TUgDWHnxtSq5JRlNFBWJZjUdFHYmNqsAbcBW2jbag2eZFL0jRD65f+pr87T0+tIWGf3Q5mJWK2uBoDbA0Fw5CO8OvEJp8O7rP+IIMQzghDPCEI8IwjxjCDEM4IQzwhCPCMI8YwgxDOCEM8IQjwjCPGMIMQzghDPCEI8IwjxjCDEM4IQzwhCPCMI8YwgxDOCEM8IQjzjtpAPCFzh7kIeP35FYJi7C/k8+YLAMHcX8n7yES/Adot/hR3BY24JeZpMJk+4hSzAbPdp7/To0BQKFynGwDLBObpYoiUqDriA3ltcItpnGOZgcEIJ/ILjNYdRhrnoVmbAfYU8k5BPuEUsNIjsoSdEipmaxQs7JMRIctZE6KFEbsEkIsYZ3R7F8uL5QUa86LAQ/XBViNxpAheYbtZ3FvI4YT7gOkosDjhjKwyANJcDQrpQnQuRYOI4xhBxgt8iWeRmQMhtpjLtty2InkI77XV3ldNJ+5eEfH388Dxhnp8ecY3DzOUWx8fdNTJPwCTxqVgWAEzTfoi9/JmyDo1okm2jcHrqlVjGx3LG5bbI8yQCLTsqxD5r93CNyGknKxtR6HZmISLee9QOcjyNyyY7CVTLMRhDU6QAlgeuRXw17eKAKdyeNzjnnZzjhDabSpp5WclKA1amdJS1lJsfFnaSeriipKydAUBjpbN/KOTp+dv7j5Me7z99GvimRMLZ3HSPuxKx0WAWD12E7dGNFNyQmRHqKCQZgxsZxmNeRHXDnWijPONJUf7gdLwAL+v0Im/3SHU81mza6RmPjSk1WjpPfQseZBHFVDnm0e4SNHVz5a1WueB8+NapPF/8WJyvrmucEcmdqd20EyI363kt63Tu5LQTYtK1pkqLo0bJDVdxxQAZzUiN+kMh+M7euew2bkNheEsY4EUkRUhgpFqWxkZgw3AcI4M0mwLuSxQoell00QvQ99+V51CKbFe+JE4znJQ/YJuUDinpfCYpz4z++fObf+no/S+HMVA8A8n35xVOkx0gHEilHRDfhEiqkFsb7vkIaIQNBTW+W4NJT7BrVzCEIEtduQIC8UG56zBzmXgGUroA241LIgGIdYWGZng8D8RXhlLETG4Yb4HMCOR4AqNj1AKBC2btsfIJHEGQMW410lXgNDOWXQUEf38c6g8yLEyIoKIDkpwAQpRkTB8CUVRC0rpwg2MKG9UN9tTAZggWPRDIq3/XrAPSBW0h8UkHJAF0DBBQ2a4h/Qn1QI4uL7c4MpQHIhGIIgjCA8k8EK9kZcxMkIRNEEhLhszE1WvIzz/t4fjl1+O3MEWaprTZA9JPWXJ3yjK2GABCikJR8wxEWUY0x0Z+gS/TASBMY/ggkHIPCKPu/AqoUf5iICgcDxcBEbNVniMQ5YEoD0Surl/Uf9gl8tPxZT3VlVNh1QGQxipc8eud668rzO8hEE6lJShszyynGTbSOELs8AhR0KkdApLWu0B0XTnpAjp6BZDpCFf27BIgYzb1U9YTAjQSt+D2K4Egkb8v4YHrOc7YB0AyvHxDecsmhS/z1iVqf4Tk2AetdoAoaiufNGZxiREDQBRl7k1XQ0AMsOAIxG9wkhZGKpzHC4GAcwMRbHkJEAWfT/A2Q1sTF8275tcDIb8/A/mbHBV8mUFFcQDEFXVa0gXSqpvSUsjtNrW27oEoWkgCSRI7QEhBc580pXXTUEYOgBQ6Va6vtNE6GwCCAanGPtvk+4Uqc92V9oVAllKKDSb8PBDC2cbImYEYbuYz2Td/CyDf9PrrOBDe3RDCxIkn2ylbVAutE1+SOYPoqhJG+lYGWpoKLlFq4uXb59KP97ZBtzlhGRwDu4OoRcWWfoYguD3pgoji1UIgkD5X0vTngX0L5jt2r+cKGSIijMl2PfQmYgwGSxP3WsIL+uoCcrNa5hi0MrmZQ/OpWWXkLYD8ugPkV/JKZbom57WtyJvqph26X074411u3vSPTv7Au6vfcG3/8/Vndj4tP6ZvnLvEppzZBfmC2sjpPWfLtwSCS8hvf5Ef/B8w/odKC0PeVllVrBn5osq53EzImwL55Zuffu1+kvxCoq7T9UB+/uXPfvb6Jf7V+rW6/ofhD7uVCOS84j8D+oCKQAJTBBKYIpDAFIEEpggkMEUggSkCCUwRSGCKQAJTBBKYIpDAFIEEpggkMEUggSkCCUwRSGCKQAJTBBKYIpDAFIEEpggkMEUggek8kI/9n6UHp7NAHu4+k6jjencgavSJRB3XuwO5C3SJuU8ujbxJyFekc0BuR6PRLTkp9JhJL79qjJeKXCfNyHGZdKd7ysmBSrlfOadktcLPqWQzM7ymSvFOQJLRp9GZXEtacan1uQw/mrbA6ALj3xjItunLNZUngKjCHKkkj4MPejCGMYKJST7jZEhi9D5AHkZ3jsn4dGoquC7bnAzq86LwMdCMyjcGUqZ9Xum6GAByXgkd2LgRS45ApEEqOzD9G+jQcGZvz7jffR2Qh3EyUuTb0eT24VSmEQXnRJQGDGI6A5ik9Y3hW2klL2ldthct26vjsEGUCeGpfIRQbKOhu5SVNDnVHwKRZe6quPveNYQaKq0FFcQ3+Y47IKrkUIIqA3bcvaRr0Va6rpvvKBkUAhmLCUxbrGUnV2BAk0hvPMOn7rXijG26nAvwnBl78xk5hWt+cru5eh2Q20l2NwK5m97PI19KkuHFpLDcF7j3ftFcrNEApjJCF2jrIkUm6EK08drH948lM7o2XGvi2hSu0LR+Mjv9Uehv3feXAxDpn5dmglGhRF2IJfHSnNQMC+BY44Ak1BXWtGZiAaSgHaU7FfSq0SQ3x4EcPnScgAHNhslsIljm1xDJxDJncJUILluOZsKbz+RgDaZmzreGy/HrRsgnhyBJJp8A6PefJkni6kfuf1VFbfojIZ1BDEdrH3xGubN12Zs5sorq9GYPiCXeLYXppbdo0NVAfxlsUm1/mnFIpY8s0t0pi7sAZtsCUQhEQAFCLcDAV19BM5S8ctDPAekdABJvQDNFH5MWCETNN+1VQlY33G2Arc+PYo9n4pVT1rcAYK/6LTmmTNa0UP3D/ZhAV2RsjUAOp/JsoWmzC0RjgCRlzTmXNMcJabi/pO1Pb3EtuqeNa1LWO0DQekZBswb79UC6M9DI4KBSawnfkPNARkgAQzvzAIDRv3AEocZTY6SrQAsEMl/hirR57Royvtsh8uncza+x7CCBqraLQSAoSeU+EMyOtgXIAxnsrwdCtcbdNbSoeiAuuE7T1IJVxKVAFC+oNueBPM0EuRDIaManOecY4oFIgUD4qxf1h7tR8hlLnyeju6PLuigT79l3kECcL/i/gfCyNW86AKKoxO82CoEM9dcDqZQuYI85vMviFs1wqCKNvQwIaLmgZ6csJQW5FAjOVIbjhOaB8A129coRgkSy0fd+iT/KAxPZwLuuDhLIWsOZQyACM5DQxnuWNTTpLGYywqAHJQeBMNtF0xaZoC6yLvzP9h6IrrwnDgRA2/NAOL3HA50BoqQhFwORG2jAW/MZw+C2S4ExTf56ICQZtSNklJ38YVg2jdXZQQJzq9PS+gSCat10N1clSyFeWc1KihGaNbAkqFqnja4HgWR7/WGEpAIOwxog09gm68x/QGvXTQX+NBetIQ0eVtlTQObMgPKLgEzZxszAfGYykys+Y4SM53K1mhlyBRDg8PmTGxwZkDmqjFWVXPYGMXiiaPTCMlcQzEdVVRe/wHgfAY2YdiXpL93t8tc41J/s+8MIyXHzQvjOEAju7lrzapHLvHPs6bLHxEGF8PaMjqQrH2GOBcqnS7UGNLA9618kz7s2xmQZ9L1cGTFluAloXgHkdjQh3+Lt7hMs6f+dmCYfV+PWWq7VFUDgJ3o2Gk3uRndPh79BIpCLlcvpvXkbv6wEfhw+kIcJfJL/ULwiH1g5lzwnVwPBfXd+pvo+1L8U+YA6AWS898Mw/tX6tboeyAPp9RCBXKD4z4A+niKQwBSBBKYIJDBFIIEpAglMEUhgikACUwQSmCKQwBSBBKYIJDBFIIEpAglMEUhgikACUwQSmCKQwBSBBKYIJDBFIIEpAglMEUhgiuYzgSmazwSmaD4TmL5a85k3lbpRbYG8UgGZzwg0euEMyo057+KTdlYIaXMm8jt1rItWgrxEzLVobobtcGjb1fDuRKzwM5+z2eqI+cw0GPMZ72JRU9U+Tt6pSY9cu4sEMapJL/NIDpWvsyPnxBirrXvLyQUqZVfQ8Jx7ql4IpDefGbFVls9WwZvPWAkorAHXhZ0UpsUxIAwLeg8Ip+RFSvWLXWpKOKHMNi8FshETjkBWEi1MDtxlxi8xnyHvYT5TrAnhOm28c4Jij7YURJXWlsZdIZi+EHJf8u/q9tpTi0OCVpDT1g+m1LTkXQ2dYR5dm4SIEjrIYZ6Cjsr8AAhuJe5opm2ZpI92m5DnhrykuuyBwMBU2OwRh4p8xPY3lLtoON1TT+Ga+aH5jBTJ/Jj5zPSNzWeSf5vPTAYXE7QV2VZcE1KuIVMGvGeUWNdiSQxdgDWMu2K74C2Qe1d3LdZME3SU4ZYRCMpcbeFqEpxhFtJFJuBnw0VtXaCtDddU7ANJ7EJIymGAMrGmwtvI1BpGnMaGmdCV6IHgtIrNjC4ginWnJ117dRpIxqZESbNnPjN/ifmMWU7m15jPZJeZzwiXOGuUe6fcP2WsoIBTVo3WMAV8BQkK0lx4B4ZGY149UU7JTo1WPrLzn0lIYzNC8kMgmOOm8JZPikr/ELXBhr6AU9YhkJy1x0lrKHA4PdxzGggRbMb47UvNZzbvaz6D6b+hitTyhuaQR8lYB0TRlHO+1TBJ90CEi0415rQGtxmGCQR8pas1QJY9A0mw4LOZHAKhW9cgpZhL0raSLMWGUB4G4n1sCkpcVPqjwkBxAZCn2WqUS/Fq8xlzpfnM6FLzmSJtCviiSotfvKoHkrTWMD0QLFlGNMOcal2AFCSwrw0BSQeB+AY7QEq7OA3knhqSaL1AIESsLU3VhUD4Bm+11Bc0n5l89kU0nzmqpq45IcJu1+1sRTwQXwMdAGGW08yPkJSgWiCNrwwA2dZDQKzESg8E7/NuTgFpwHHLLtGCBSVscyEQzOgTy15oPtONEG6uNp+5O28+4y8Jb3etla07zz0CgRQWONMkB0AUtRXm1NvOKI5A2lrCh4BIKPBDIIVWroHogWA8OwHEQNeNJd4XR29h+/rSEcJxsV5ebD4z/jLmM0RRzM4Wl5Bap6mlEtKSCpJrzRrLDoCQggqfU6XB9sXRETTlUGNQGwCCgfYQSOa774EojQ41+Q6QQjfKA7Fp+h26nglaN6WmymHesi01FwLJ2Fys2Iq82HxGvLP5DJG8d30BL5gM7jfUohJYhY+EdUixlLNn/w++rrj/dB+qrTHhL7nzn1HQ0XpheiBGPvveiC4xTGAYOt7gEXzDqvJAOGNMZp39jcSuc9er2HG4SU6Yz6CNjHGBF5rPbP5tPrN5E/OZu2/RfOYL6771UPzq9LbmM8lolDgkn774n8Hf05QzuyBfn8B8ZvWVmc9coqyq14x8jYrmM1+1TpvPfI7mM5cqms98UMV/BhSYIpDAFIEEpggkMEUggSkCCUwRSGCKQAJTBBKYIpDAFIEEpggkMEUggSkCCUwRSGCKQAJTBBKYIpDAFIEEpggkMEUggSkCCUwRSGCK5jOB6SyQz9F85kJF85kPqQvMZ+7I16Tkpi9coPt78iJlUzKo/4X5TKnIEaVOLCFDYvpF5hs6PRsyFjM2z7urlcfCQjSfUbvmM7J5ifmMKP/d8TojR0QLtigoex8gRo4ywbIwgFxgPqN2zWfy9zGfoQyJivcAoliOBg0hAHl48OYzT+Pz5jPb3nzmcc985rEznymHzWce98xnHg/NZ+7dJsgIfPIy74G0z1xzPEB73ASAmBILqZaPj2nSHiNVWPBb+FY+SqIad7ALRsjyFg7DOyDgJZNh2dvJzBUU5mwmuEvk2Ljd+dsDGTvzmbt985nstPmMPDCf4UXNsz3zGblvPlPsms/wivIcap35TMV68xm9Zz7TA5HU28dIyv1xa6sIc4FoLJPSwnAoJLpwW2o8mN/FaM0E2WownEnJJUpY3gJhPMvmM19RsxXYyUBhnmUbiOES3GhOrvxXmc88ofnM0yTJgjKf8UA4BHA4Sur9AVQlXPPMo0qtX+B8rxk1JGcKd/mYezwlewkQKdmqu9jZLcGOQBMxRq8N7yYwZnn7+PdmRk7ovc1nkovMZ9hV5jM9EKI4w3mnst6+j2ncg3T9V6Ou/GLhnWnWtI2RtN18XnluZv2i7oF4jVZmw1RrYOKACEb8/rcHQm4PzGe+v9B8hr6D+QzuR7eSe1ssEAjhBdXsAAhGagiGDpLaLnog7FIgKM4HgCgpV8LsAZGdy8NxXWU+0y1eo7vxSfMZ+ULzGXOd+YwHogs/JyItkKTiXyPE4DGwb24z3NXFqIuAKDFGEANAcrb02TeyBTJlEAzvJ3S9+cz4zc1n9BXmM/1tb+4TmugUj4tvPRCbAKLM9/ojsLLKbe6AKOgkseeBMDjofGiEiNnYvbnKqLPmGGOw5OSE3s18psB01brZNZ+xZ81nWKPXaD5jzprP9HdZdZpay3FQpI21a1hDUlbrbAcIHJou2mPYBRxDN6WlCmPwBBt9Fghk3Ag+mwwAmcyk4Yy5XXxmVpLlMDiMmMslOa6rzWe+fan5TNGZz6wFVk+Zz6DdjMQ+1txFX2g+w5y4v2rXQhiJhTVbPmcMvKry9Zp3MVjIqjWDHjHGN+WGnFW+2ay6FGfT7u7Ku9FsxFJMwD6OryYshwCzEad5XG8+o6L5zDklrbXZcUXzmXcTsjCjqZSnQj6i+UwRrvnM0syluHQWud58RkXzmbOK5jMfVNF8JjDFfwYUmCKQwBSBBKYIJDD9054d2wAIgkEUvh8UzE1hSCwsadh/NEmMrsAV963wyucgYhxEjIOIcRAxDiLGQcQ4iBgHEeMgYkhs6/+sfU4mxPI/a7/GHYUch0kYZAWC3UUkjM4AkBPZfASXuy8yZUw5aBIi41X3RFtsi4LpAYXoV3LKm6wuAAAAAElFTkSuQmCC", - "description": "Display and manage the 'last viewed' or 'starred' dashboards.", - "descriptor": { - "type": "static", - "sizeX": 7.5, - "sizeY": 6.5, - "resources": [], - "templateHtml": "\n\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "", - "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Dashboards\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" - } - } + "widgetTypeFqns": [ + "home_page_widgets.getting_started", + "home_page_widgets.quick_links", + "home_page_widgets.documentation_links", + "home_page_widgets.dashboards", + "home_page_widgets.usage_info" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/html_widgets.json b/application/src/main/data/json/system/widget_bundles/html_widgets.json new file mode 100644 index 0000000000..ad6b861857 --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/html_widgets.json @@ -0,0 +1,15 @@ +{ + "widgetsBundle": { + "alias": "html_widgets", + "title": "HTML widgets", + "image": "data:image/png;base64,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", + "description": "Visualize HTML with configurable pattern or function, HTML with attribute values and static card for custom HTML code.", + "externalId": null, + "name": "HTML widgets" + }, + "widgetTypeFqns": [ + "cards.html_card", + "cards.html_value_card", + "cards.markdown_card" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index ca2e16e4dd..4187bd63ff 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -2,504 +2,37 @@ "widgetsBundle": { "alias": "input_widgets", "title": "Input widgets", - "image": "data:image/png;base64,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", - "description": "Various input forms that allow users to set location, image and other configuration parameters of the target entity" + "image": "data:image/png;base64,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", + "description": "Various input forms that allow users to set location, image and other configuration parameters of the target entity", + "externalId": null, + "name": "Input widgets" }, - "widgetTypes": [ - { - "alias": "markers_placement_openstreetmap", - "name": "Markers Placement - OpenStreetMap", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAACYpElEQVR42uyddZQc153vZ/e85J+F894mGyc5u2HOJnFeGDbgGOLYkkFgy7IsCyyymJnJYmZmZmmYp2mamWEap5lhyHnfW7enpqa7ZzSys3t2s0/nd+bU1HS3urs+9eP7u2V//vOfE7kPyy09JxQ9R6R/tXJB3SP19kg8PXxnzwNT1xM995isp87ayXN0VGtC+HlO2f1X/EUVyy5+x+bGzFAeCYQeWnrCmQ8BVRmoOqV8zBMqLD1qfw/f1XNd99/yq7mo7rmt77kvss9fvmvNznOPtMnLiuxgFKoIfzWa4DVF8oK6u95GqDp2s+n8QzEOHhg7j8vIw47Le47K/vrB2i/qPCx5gnvplKIHUJVBVw3l0bV2PLon1dHTnuqpsZGb+L/L93JGQajaf0M85f1V1+s1x6433ZOHHmoSivYeVqrMuQeGDvxstuf49pw10mOL9Ow+frtB7eU5coAJ0mRJ0wNIi72DPhFa8L6h43+UAhuKlFt7yoZuAaGu/CnCFsST6Ln230R7XdcSsKbM3XilxQGeWLnTYly8eu/qLcc27jrXoAuJ2rJCe/pylbxK4XnuhTGQX/962O9/P6JWEwBJtxt00+esmb982+n7AvxapQ6t3HRk7tLte0/fv8e3/H+SCuSkoqfsiZ5wVtljj+bZggKrt/83+JA3dT3HHygnzVwPmB5o4vfVhKrb0uCcJR9Utpr3nrwzf9n2EzcaANbVGuWfXho/d8l2vjlSIbb98pcvb91/kWfPAqM3xs6++Eh8uVyyaOWuWqXzbpP63feWfnDw6l2eCfL/SSqWsid9AoygyJNnC6IJ9JyQ/5f+hFc1PRcqZM//8W3wdE8VuyUL4+B8i2/mgo08YwjyoFk7b9kOgFWragdYVyslFKw/PDu6TukBWGfuCaDAANaVKvmb4+ZeftRaJbXPW7Ztx9Ebf8VgHRR3H2jt+s8Di+oAa7grke2mbLnjPVe1/3W/IPiS1+q0v/nNa1d5TtYO7r2vffmVdylYDTLbxKnLABbPEn95+MQGrU9htCczuddHvgcfK5bMljfKAdaaHefW7rx46HwFqKJSLbWv33UWYD00/hW6WXNuhbbUZ/7zwDqt6Lleo9hz7NrJSw90VjdrFoXuntPKIUSk8u4b2s46S6ZO4bmtSXP/9B8XYTVa0guW77heq6oQ2wX2pN3tVWj1z73wRjrXmc51bd158OLVu8FYQqW3DBs+CepKYbABrLfHzaxTuhLprKc9+NqI92qVXqE92epIiw1eli3IuAkLhO6/wgTExvp0ibtU3nVe2XXsPwKsh6bu1R8c33v8+u16xbHLjy5dv5/IdlG8YtmeOn24SpuXOmtO4iN2846hj6dWR0rcRmTUmJnnHrTyrMkmc5JnSQqsCZ45huPb2tzRv9BXs7vcvr/ej4O7+k5RW2bS1KWQ96Yvr20QRhLJSCK1eNnWhUs3Tpw8PxSLQ9z+wKg3ZggtUXN7EmDtP3Rm/sINRksbjgOh6LKVH6zfeujMhTuJdE5lC2zcefL9+RvhvzfoQw9NXf8T3KZb+u472hzkorLzLw9WS1v3uImL79SrYCYgr7w2Se3J+OO50+dvpDu6k7munQfPNshtzXo/ZPaizcs3HNh96g6FicrS9QfembTwN7997dnnxjTqQkAKcuDsgxnz1u85eadBG3ioz/xFvohtZ2r23xAh23RG0d3qzLp8AUub2xeMACkqvmA4lEiGE6lgIkbZAkOQRCabyKTp8UCi0pnviyx1uvANVfp/iD9eZ+8eO2nR2XrrDU0/039RErsp9p+VpD46WLBWQmfHm2/P2XviNgVr8vuroXWu1SmHv/ouwILs2HuioqalXmbefez6pXLx5QrJezNWNOsDzYbQopV7Zszb+FDsbJLb33hr1vkHIiDFN8fGv7fsVqP20IWKFZuObNx3qcXR+fG/hQemnniO2GhIi4ucaXbkgsmYM+WyZqzmjMmcNpoyBirGtL6WX6sJq80ZoyVjNmeNEEvW5MjafVl/OBMvydZpxf+gFDzuT56z5423Z+86/ehmL1i3DD3nKlU7Tj48eb/1rsR9R9/xEcEixQ1dGLE35P0Fm96bsRJUEbBqlc8+9wYHrOZ7j+peePGtK5Wyq9UKMHTuPu/0zUbEXIjYRa7OCpln9JiZFCx4vy8Ne7dC0ga53aidOmd9qyuf2v44Um0j1QJ4fjf1zC0h7alsC7AkFYjKrwBYUocEx+asgYLFFWvW7M54QtlIPJOhVMXSuf9R6QPkmQHWWxMW7Dp5j4KFrxRf796zj5ZuOLJ807HXRs+4p8+d7b3ZntgU0qTO1JmrJEoDvlyI0tdhavP96tfDk9lOgLVz30mAdfXGA4C1dN2BjbvPnb/XAgt4q141/NX3mo3RR6bOZkuSAxZ/xOjpFKy9p+5NmLoCYP1lHfkTik5B0DkQVZBmRTPAUrhl9NdisLiiTdovquDDwmv8iBoLUU6VrUfs6VH6e6S+nqa2nhv6/1rVoUPi7uL3DLDGv7eUBQs3v8RLRODIIjBas+3EyXLVTW3HRwSrwtz17pRlVfUCShUrLw8b52kPCyWqufPWAqyK6uZhr4w3WB2hWEpojQEsoS02+q3ZLaYYLAiU1phxcyhYkHcnL6ZgLVl3YN/Zh02Ov6QvfN2YVMWtg1Al98hAVb2o3pjUM2eMg4PV4vcclfRQOafKPtGbuazpMQR70p092a5CCWd6xN7/1MQN7M8Vbc9dIxEcsGW6PYKONVWJwmS6vBesUw/yGkvW0+rqOnC+YvaS7TMXbpk0Y+W2Y7fu6nMfEazzqu4jl2vafMECsM5euPne1EWQ2XNWASx3INQeib03bfGy5VsOnbhi9SeV7uS0WWsA1ik5AWvpmr1TZq7ef/ah2NVx/kHryDemjxg9beWWY1BXN3R/Gd/lvCYjDAymqKjwlC0AS96nrh6jsR7YIixYFzRPABY0U0mkCsSTJCbmjrH7+H9Y5vmOsccc+TDX/edwutsezEJwgF9x8rahZ3VVYl11sphCgDVq7GxorNuMWjoq7rj0SDJm/FyRPfNQaBk3cdHm/Zfu6Ho1VijdaYt0idxdV4es2JvsnRQmfzbszLXZO2z2DqstY+NbBEKD1Orz+KPxktIejQstsXJTJ8BqLRK+PcNr67r1sak6Kuu+YUrw/S5j2vBYqiAt8ha+msf+OjhVEMDEggURBDyKmL3cETouH0zRVrsDlozDm2uPdMTTnR2PxQsCCn2pHkOIpGwqraQ4+/GLHGeUPbboh0gMnSxX/mnOwadHr2XlT3MOnapUx7M9ukDXQVFHSbBGjnl/7rJtD/kmsT2p8HYeu1L7+qjpt5sMhy9Wj52wYNOe8/dYjZXMdbJiCnUOBa8zym5Q5c56rR3mkmLLWew5G0IqiB2Ss+IMFX820OomYMEgsoJ0AJqcTn2MIAuO5BlV7pY5Xu/xaZPmQTAyZvSIAblnlB4F4kEOWIOxpUtZuFQx1rBDGbfr02ZVwvbAHjkm7y5+bw0+n6XDwBV7p8Xd6fR3BiKd8URXJt3VNRTUIKE0yOhR+In+gwk7qXgyQxzL/blC5vrF+M1cpLjyi/FbqhWerp4P/enuGlvnjnum9Sdq8JNGhQCLyrhJC3ccOqtzO2bMXjX8tcmQ6bNWHTh7QxtXU+kHFiSe7Wxqe8wF5nuSwVyIiivnsuYsAxFWCFyHpT3jv/6o1hRwXVBn/1KK/aEjZEgZh6KcDCm9NCSVR+QF5xvEDUKD0JDUkV/7k2TLWqCV23IOW9aKX8UeZwFYkFPyzmpnQBJpA3YQbcJmDnlMIW+lJXFM3tUScHORMmXwv+gLOIPYOszODoe709Xe4QNwwa4ImIt2JmKdqXhXKtGVTnblUp0dma7OdGcXJNPVDc4yXT2BdJesvfOWgehL/HfHBlCc0FWg6tgDxUBIceVmk6jzww68pR0nry5cc2j3mevkZsi4lW16ld3sjAZ86QgVU8Rap63X+HT+XNCV8vaBtXnbgQK2IMr2roGClGprz/WKqusP7t14dP9OVTlA8WXbXVmXI2cbCCaQB1sJHebOuvH4izdvW4N2ldf9l8qwF1BiSZhkZpXYKK8S1xX8SRGWi4NiTVxdcF7aJqlvrRcaBAxYfRrLkXVITUqFRa2wqsUGRVvEpXXZQdIJaTekmLCzyo5HlrjK72uLui7cuHnm6o32TMDaYeICpPDLlX5FMVh/WYEGlcftgpCrweurcIbuWGOuZHeVrK0AoNcXHT9yV3zknnjU4mMFf5JaLMmehLBNvP3YBYlPxr0BqDBfnY4rhpS2D6z1m/dUN/CL2ZL7SoMv93Su3bLvzNUrZ69d3X/0ZGVzPVih4kl5Id4sIxmfL9PO/qmkuJIhsTMH513g7qm0wKaQli/oW3TPNTlJM/ETgWVI6ahTpQwrfvrzP37r27+CjBo7WRPQ5o1g3KTz2AyRAQ2l1CkpAKs95z9y+tyM2ctHjHrv98+M/N3vR+idJlvEDW6oWMNEM+kDPmPQyz1vD7fNXbh698Gj+JhtWTv3kst9Mnm77D8arAJxd7blujt+NX5TAT17r1V1ftjV9WHn/usVBX/61btb8BQ8caDXLKAKok9q+sDauHXvngMnisGCwKMvvIqybovPOnHKfIlOrnebmqTCxSs2UUo0dt3seSu37z1UQI/cpC44xn1Pf9W5jAgMp89Z/8GBy3xLHA2ZzcYwOlhmL/lg0Zr9d0Vtj0XqvjUsCdkpFvqk1sQ4T08//YzYIH8krYSo2zXGZJ4bfbsNYAm0cku7zRq1mlMmkmvwqnRxTX/CjNTNsubMgVwQn2jRsvXlzXXlTfUNEv4vf/2S0WdmARJppdaAg/2VFbVd//7cFbsYsDxZN25llVut8CvUAbUz7oY4ok5Fu1wTUf3ngAXdc7NJWGzywFOqJwkpBgtyu0WU6I6bc/qhgMVVV3mwIJFkhotULJfxZyK6mKt/W2CHOGS0BRyz5q5Yv3X3nsPHIYdOnvGmfBQUd9yrbtNfu3+P/nr8/MUlqzfS45MXL23atpsevzl2uifpNXotY96eXtXccOHmnWVrdy7fcEjmStQIxBOmLLv8iN9i8AmtoQvKwdpRal1+LhD4MHDMcfD97/8eplDul3H/2mpGS9abr4+a+ItfvLR6/TaH3wW59bD83UlzR70xZfKMBc++MGrzrt3WlIWNDVHSoWAtWLIOSFH56ld/Wt5YA3Sq+Y3vTHj/xz954fk/vakJaHxpvzvhhd18beS770yY+fLwt4e9No6CBfntM68+89yIA6ePtxoloKot5np5+LgZc5eNnzL7RuOtAYHIGcwd+o9PlbXD2PPn7lfn7X1SsF6dv6/7w25jWmdIa41ZnSmnN2X1xozOnCsBFldd5cH6YNchd9ptIM/vJ40+d782LFNEk0Bd3zF+0hwWLIjapqVf352qij2HTzz7/GhvihjB8RNmCyxC+qcxY6cdPXsOB6Dwueff1DkNcqt67DszqloaIeUN9YuWb9Y4LEqLYcr7Sxet3FGjdLZawhWG5EAJhSafhxPoGXQJcrvAGlLCfv7LF0e/NeXI5TMUL31Mq4/qdFGtwqk5cfES2KpsqCdgPaj49b8PU9t0eo/5yt3bY8dNd0Y9jwWrVSdTWDW/f+ZVgUrSLBHWCVuAMrSgLdA2fuKc7fsOG/zma4/ujR3/PgXLl27/1b+//MKLb1BdZfbbnn/xjVsVD3F3OaOul18dpwrLC1CAksC11CZUEF1SPZDOGKK0ddoynVkWlzeXHb5Y1UJFZrWCqmR3QmgwHrtXS2Xk4r40BJ5oy5jxZrhSDJY8JG12tkD4br46qiRg3a+tKm+q9WQ9xWDdsUS5l/OaIa6Jq2AKX3nt3VpBs8ahx/V48U9vmX1W0DZi1GSevBWU/PRnf4KNwxc6cfJcbZvBnSGGb8LkuWK9HAcmnwWdmS1yEfz32fNXULAqmxrmL1mntptZmTR92UOhmWeNlgSrxuXNU5XWwwLCltG7RJfQIJVgS9o0MdWuE4deHzVp9FtTK6W1QEoX0SqdWplJU8Vr/tWvhl2+e5MBqxzqE1gbfdZqftMfX3zTFfFxE1oUrKnvLwZSVfzG92YsEOsUUFdVvIZvf/tXfKUYbEEAFmiraml4+52ZlYIGS9gGWbXhAwoWPinAunDnOhQVwLrfWPm1r/3MGrADLMiMWUt3nzxSYGKY66ehYEFw/HHA8nQ5UX3naik4VfGeKCTWHXFkrY6sLdoVodLZ398KJqLOnINFSp/SauLKAlPIUkVFGZYTsKivbc/YisFCZoh7Oc+pM9BYiI9eGjbu3PXr5nYbZOrMRaAKMnHqfIFSXC/iD39tPNVSi5evd8XcCAlxvO/ocXrSGXWjhigzK/F1r9m4vbKl8crdezg4cPwMFyzI7mPXWm3hkpGjKgqLrme0VD/1WyBXqm+MGD152pzFAKtCUPfOpNkSg+rmo0fP/GHklXu37O1OAtZb0wRymd3vrOE3/f6Z180eW58BzRooWDD9VF01SgWWgA1gNUkEAGvs+Jmjx0yDvItbyGkob6gFWGevXwNVhnbz0lWbKFhyixpgNWtxKysgd+offuMbv3AnPBQs2OIpMxdydJWu9xL+JcEKpQrA6oLXBUUFFwrcQHBApCsO23fwejX74BD6QThgQUxZHX1Z4lRF1cqASurLgyX0ChVhWd4UWjqMkGKqtEm1KGi+YYrCtcK1PHy/9djVewDLEDBOm7UEN/qWHfsf1ddSqiAf7No/f9GaJcs3lDfWUoZwKwNZS86In1BUNGzEzyNnzuk9JoC1+/BxfK2QuQvXCJQKytPmXfvnLFg1ftJckTVUEqxrhpS5wwiXfBCkWJm9eOWcJSsB1sg3Jk6eusDgttjDzuGvvMMFS2szO0PeJqngmT+MaAt4pDZpPm2RMfU67xtUDp0qoNDE1Ka4wR51SA0KgCUxKJR2rcVvc8bcQKRBzAdYm3fsBVjKNi0LFvQ3wOIbhZqYEvKQV/nD//sHShVk9Jipp++cZyEwZPLXDxaQA5b244Dl7LTnujpYVmDpjt+rg8DqwQICL9YUnrjbsPvcg5fe38U+ONfVac9aOFTljTIO5D4FFYVPqY/oIXm7EYN90JUGS5vSgKE+iavu8Kru8atxjGeaQ1aWJ1vYZovarPgZtOucRkekjY0BvVkPsosAC5l3mnpQB5X2uAOXQayTibUKmVkvNerEerXKaqRUGUImfUIrcAhdOSfSY46UW520P3KETytp1bP7jiVuyJjxhmHyBoLpxK1zzZbmFmtLuaxq5Ngplx7eBFgj3pgEsBBYPOJV/ejHz914dI/6WG+9/T5gcofbW3XyN96a6o60G+MGpVfBBWvZ6s0wYbqEmhVtRLVs/YZ33p3VIOYhy1XZXAtEoJ5nzlk+feZSgHWvrmrz9r0ULCin518YLTCIYEQgbXHXc8+PnjRl3pU7t2+UP0BOhOOt69lLyFIFMWa1H9N5//DPHz4/Y2exe77vegUFa18p5/3F93d/+OGHdaKWejEPehoXzhFxKh3aZqXg8sPbQMoUQrLU/aC2+l51JVya9mzQm/Frg7oWI7+M+QxqEAOvngVLFVPIQzJZSNoPL44QMOPk8eBmKGLNmUwpncwvkQdkW3cdXL1+x5oNSOpekvolsoCUFQQgFPQCgbeoiMFMmNgzA4FljpklTsnKzVtGvjF5zLjp91rKiYMFUyisnT578Yw5S+ctWb101cbyphporPLahlXrtoEqiNqiP3vpmjPiAVgQJEubZI1qu3rn3qO3K8pxw3DBgkh9YoD4+siJ02cuuXTrJlU/MpNq7eYdf3h21KmLlyBX796h99i5G1dFZjEFyxK3SIzyBcvW/PrXwzft2G0LO2w5c68dNPSqKw0XrI/pvEMyPalzlU1PCtaVKoHV7pi9YNWs+StxTwIsuImTpiw4eOrM3uOn5i5agzOQZoUIYJ24cOl+TY3Jazty5uKkqfPLFFE5iwvsOgIrdUwlCYipqOOKAdlKqodIFRVVSA6wIPMWri3gCbQpIwokxJlwozRYMA302Mr8hM9egJQiohD7xeQ4pgFJrY5WpV9JqSop+naTsc1qarNSsKCrnBGvOWahYBmierFJxFO1ENWb9lqSpgKw7Glre67dn/NTwQe0ZaxtaYcr5WbNHL50U9KAnlWki61JMwULYowZwJMlaCNU4aCPgLzG0ib7qGI++MfNOPi7fJ5osCRYXSRB2lUSLLvbd+b8tRpBM7yaJas2UYxeeW2CyqaVmlXjJsyiZyA1vMaDJ85ADp08e+rilbaou0walKgTSsqKMiJnkaKiiisHAgv5jKFThVuQUlWjqtt99BiXKkRwcJNL8kSeS/VTRkfBslLJGftRFVPjrfI8/DxYfec1OgaygaRF0gKhJA0krZpWBJssT7gBoMhxpjBxnyX6BoJjcUDMCgtTgcjCMir48rkEELed413BmJhyuo9JFRQeUr49f+4Zu/xIATrwt47cqT52p4abYqAyYt6+zq6uHfuOKK1ayPptuylDY96egZ9Imox7l4CF0GT56s13Ksv3HztJ2Tp3/YYr7mXAihF65GFpAVWyoGQgqnQpzdCpgtmSBSQUrOOXz1+4fy2vqIIywwC2D6LrvV+pJ8vVWLCYXKpa/a2gCiILyXr9R7V2YJ7yGiuqh8dApaaxRqARDsSW2CpiwUJoLQtJaMKM0zJPuDczbKnjSpYqaUgyOFiKqMKS65/Ozup1vVSBMDYEezKM+ptOvCB8GyQXFHbbUCrQVO5V8trcLgTsoEpu0UyeMp+CNXPOCmITo64Zs5fhACELomaVTUepetRQi8YakU5ehvsP3wW+ggKqABzezQBGUEW+jqFRZc7qFUEZpQp+yakbl1rdIoYqKVPkNw5k+7hOBkOYJv+nbD/vSszoKirqKHH+TAmj7nFUQVDVAVI3yx8dOXO+LeZsNbTK2qQlwarl1bJgQb9CsOair2MC3l6C6Bg4EsBCFpayYJEvcACw5GGZPCJjg6z+CXdChvkjaSZjRkssSf/zuMS4HxydFqQSxiw/NBSqXpu9p7unx+S1nr9xfenqTSs3bEPxioJV3lRHD2pFLfRgw7Y9K9ZsPX7uIqjCr5fv3p45b0WZOqZQk88pJf99mIgyqlDFBrSAEALEEKkiJeG8ayXr71cxIbRxEOH6Gb0hNxPApvrUFVQUS5WoXcSe/92zr744fMysJctmLl5qiBjMEYslarVELWZUoKNm/LRF7KAK+Vuo+oMnTgMsW9guMUmKqdJHdJV1lbp4PwfLxFnho+c42qqogqUKfsVAVEHwJSPZ+JcqBYJClFxIqgLxY38VCNSo8cFxpCtscLuGApbV057IJSk3yA2ZmZTK0AX6rGwQgErK0CNB8KcIyThU9YGFaz84VQQskuZQ9qorFevX63prUigd8L18FizcGDgJdTJt7kJ8F6yZKylmP1FXCouGBYuyJW4rZEsX0lbUVmhjKvBEAueUtqBPkKorSpUk2EtVtJAqyh/7K44/JkkWUkYkus1EkWLEVBQ/ahJKxv5IUXZEwxn89NVHrw9O1crDN7p6ulCk1ydR2NDp41pVRKGOKA1RXVvUCW5wl2pCKomttVHW0CRvxF8hmogaty7+evryFbjwZVomvhsyVdqhUpXWIdZjqXoormCpUoUVBQz1FjF0xAXOq3QdbLEiItfm/TkDx/dSU4AEPiFLFWMHlQxtqgnTZheThD6WGmEzPda6DAdOnMSByq4jYB3Pg7Vt3wEUj98aN23b4f0tBn4+PIzpa5prehWVvtBnTxOTzXiosj5dFS2hq4jfw8S2ec4GTaabmVpvge5heCIYGXsxKpbil1IwARnjzpLcmCfrSnVkfv3ugB2kv353SyqX8WbduIGp4Iul6EBQGUPGQeaRNikaQRWVioYKKjUtNRa/HWLy2crgGZDqwePYwn05dKr0CY0cDPklcKduVN9hvXXqsDN3FRcsQ0GNkwqKA0jMco2gpTfRgMQE67BTEXLsIASp7dkLVl6+e0fjNFCY0IwwdeZienzx1o3Z85fjQENNYS9Y/kzQl/TDG120fMPqjdv9uQBlS2qV5sEq1eiMdyiP9PlVJPnHRSqhNJKWEg171VGf0acGqysDHTbR0BcS5gzGnM4wMFIl1RVxsILEe0aHI2uvU90pod44EFhikznTnWapwiVgqaJCYeLpWiRuMVrfgB2CP1YEOgFc0pqW2jxYmkHB0iY1uIeG5qoboDNZRXXl0c2L969JOa4V41j0V1dZfUmw+vLOGV3/xxskQQmXKgjUG0sVKUJHVbMXLkcRetSb790ovw+Gzl2/NvKN9yhYOw8efvvdGTjQu41cUwiwIAdPngFYy9Zs8WeDrEEsryuHqEMqU7qg15lAwM0vUNcKP+HFcwkbuu3TpbjpBni0OuqVGx4n5lIajoZicN77XjBlgBe/6fSdYqrWnbiFP5lTxmKq8Ima1c3QSXxdi9wn4cJULBK/mICFEhguFZrB2WQpm9miWdOhphVSOqqoqFTx6sUqmT1sN0SQbpAiA8TETYUpK2PeDvYTbgmWCblJnIi1GPQpBVRB8OJAypG2Z3syEDSPA/EHonIUod99j1jG6w/vvTl2GgVr8849b44lkKEnjAVL5dAOe2Xcrr1HDhw9BbAgKALiRfKeFpPv4Kv5Dyse8jQtcrcUSBmYLIOmP1g0EoS/VeC8a/vnqwaRgnAYxmQgkvC/K2MK+gAaDJKWqZwOWoBaXvzKhvnc13Qm2tK5zPPTtvdbpTNrV7Yr15Z0aIh6zltAmVcC5VTbWlsvrhscpj6qEKe7eGXIFJvjZnVYRSSipJUcKAC2noMvd4hpBTZZJfNKfQF/NBY3O0wWpwWeYLwjZkzpOIXMvrRCSXXFLcHSi8rkdbTUeS8GC98C/hrvjh48fcDutYW7QnhLsnYZtNSSNZvAEArGk6bMp2AtX7vp7fHTiQvfbgNYh06cAVjnb964fedRe9CfzqRPnb0MsORmNWlXh7+YyoPlzjr9He3mdlMNr7pB0kDfntQvFnlFj82IauOqgnxV6VwDByzKKHFCipDSJFUI53GZ6JVCSGjFEm0SRvQJrkKDtL5OUFcnrGtobWi1CBUBRpsG5FK3JJKJuEL+/zsmT9WP3lxn93m9fm8NrwaiDMklztYmVVONoKZBXi92tqqCxKwj9FZFH08VAWvN+m15qsIqOED0vTKSB6u46mfP2pwZpyftO3zpFNcIUqqatTxvu79R0Pjlr3x53ea1y9cs+853vyPXycOdoT500MeDbzZT2ggaUprCu5ZmiRjnDMfFYOF7xHuId8dGjx31sKLC7LVAgbVYeXPmrzp5+TLl6eXhY9EVjYP5S1dTH8sStB84evro6QvOuOvY2Ys6vWnpimXVdTX1jfyV6z5A9zDXXpvT+o7uDjgQmDUttYtFFgHeiSIi4zl4fCe/JFh8A7+aV9OkbJK3S3E/GHtJMpIPrmOCFf0gYFEXU9dfY8HrxytzLpMEzh/197lUIYhpVjbxDTyEUCSK8kkBE0Tmzh8oPPJsZ6ZcKKVgPWwSx1IxnoEnsPMl7ZIWQ3OtsJZBSgTVSwVeL3pjBF7hIBaQUmUIGcv+8OzI87evUbCQfqDXgDZuxzvSrEQ7kui+9Wba8ZOVV0aMRymaZYukK72iBgG/s7PzE5/8ZDQejaXjyWzKard95jOfwclmQUt70tvR0wFDnu3JunNOosAyeuRX0F+GSDjTk0b3D3Fak/pYZxRn8MhUd7ItgzWxlnRPKvdhFr5nZ08nJJgJiXytD+VVtS2CcCTW2d2JGv6oMaMeVVRX1jTNX7YRcu7yLZwECt0fwkZmM5lsdW2TxKisFTbHsvGeD3s6OjpQuIgkY1jKZnM6/sz8c3k9HV3k/eC/w9M7PuwIdvk7PsxFkpGysjI8INuVNSWI247ohIDlKASrRdMiMgtVAblAz68V1NbwailJRhLu6dlif3Eys9gUwuXigqVlIlCWKhRz8yow1w8soYnfKG+A0qJgQZCmRsJSEZCJXa0Cl4Dn4ku8Unxp2y/eX3vyOr5PRahfQQ9uGYsUBMYdNpfv5oEthALFVEkDEkqVyq2V6FVl789eduD4KVuoDSShvxZrSxav2HD07HnEU+CpSdh6v7Z6ycpN+CvyqnMXrZ4yfZHCpqFgvTT8bYlHzII1b+nqUW9OrefxUuk0LkAoDJdPe/r81a7uLolKgp/JZFIql/7wRz986qmn3hr/Vroz5cm5cj3ZB7X3v/L1rzz1uafmLZmbyiVdaSfgO3jiwL/867/86xf/dfm6ZbnuXLQbs6xC0+dMu/3o1vef/v53vvcdpUGZ7EhUIUDRm3/w9I+e+uxn5yye86vf/lKqksY7ElgZ64q7Q9lgHb925/6dr7z+6t/93d/98U8vRuJReBKBZNDT7j134dywYcO27dgWS8SinTH8pydOn7DYLUDn6s2r9yrvTps97X//0/8eP3l8risHm/7U5z+Dz/WVr3xFppBZvGZ1RMFv4wOsRk0j7m8qRFHp+SKTAKkvKlgxUd1UVdzxBikZG1IjyPUE+oHFUVfQl9xXYBwGImJHK4VJ6pXkqQooYMsAFkToRcqcB+G7BdqgFo4KuU8ipn5UhWV5nmLK/AFhSy72twIsGRjtTxUez3PzhB4hbCia3hYsW1+29/Dx4xcughv0rL340lsP62tPXry84YPd6OMDWDNmLUMjaN9qp4BDpJGgR5QFC6U6mgt9Z9LMm/fu1zY2o+8725FbuXrVN7/5rUcVFR1dHWTzi654qCMYToa/+KUvJtJxnLl+4/rUaVOhD9R69fd+8L1cZw4nr9y6MnH6RBzcq7437NVhuJ+gVLbs2DL5/cl4pC/i+9u//dsmfhMeoLfpPv8vn8dBJpf96te+cfX6ra6urlgyBmRlWmmkO0x6UXKmSHeovO7Rt/7tO22BWLYrs/Pgjpdffxkv5Yo40+n0ycsn5D7ppYcXP/PUZ+oNNZmezMp1K/B0vOyu/bt+/NMf4Q1DZU6bNfXK7Sso4oYTIYDV0Qkl1ymUCisbKqlQpJpVTRiHhDVkLFJUZE5JVSmwTJnSdUAk5eGHFICFliwmt65BgMWCVaDw8IJEVxkFFKZWbytsND0mjlG8V2JKGROo0V8xCkXskTS3tfQHi6orBSyvkglEqFCwYBMLwOJ7+Oh2xwNggm+Vl+87fqoM6urCrZuk3VbKHzXmPWRxqKBt0hsODhv+zoO6KhasS7fv7jpw/Ff//goFC4RRXXXy2qVnnxsNsG49ePDOhNkmlxVsPXhYPmbsW5/4xCfWbFyd7cxCH5y5cmblhpXxTAzSHmn/xCc/Aa9lyfIlN27d8Ea9hrged49Sp8R1/cnPf6I2qh1xuylhAJp/8zd/A13tCbsBFv6q8EvxRHKNuzqsLtsXv/TldCaLlV6wVvCxGLBC9I1RsHYdO2uJ9Dg77HiR//WJ/5XMJhoFDevXr3ekbXg15AyHvzFs1vKZAG4FB6xHtQ9RuA13B0UK4ZrNaxBsZjoy+E/RMK5P6PquU68U8MSK0MgvCVZxdoDW+0AVq7Hg46rCxLYy2QQ9fnKMYF+YaWQcUyT3saRbilxaQNrqFYEqChasWPG7ZQUZ1JY2ntgvwcOQGWDZUoSlNN5Evpf1sXhuPulqRyKeQxVoA1UI+3CMdNL+k6fyYO0+dAzQoIN21oIVLFjrNu/yhAIYToTmNboCc96i1S3S1qrmxt/+7vVejTVO4pU8EtTsPXoaYGHFzr5jJ9HwFUlFk12JWC6WSCf9geDEyZP/8NyzuGyrN676xje/8eKwF1lJd6TfnvA2r5Wns+qEbUJ30g2rh+v6d3//d+3hdl1YJ25vhc74ly/8Szwd94Q81MWR+ESdPQSsXGdWoZO/9MpL2e4sMIJLVAzWw9ryXUfP+jJp/Aqt86WvfMkTcJ+7ehZgxTpjeLVgh3/n4e3L1i7FMRcsEBnqCuCvErVk+drlHLC6DAl9abDiKkMSkT9KyCTgxbeMk2K7qLTGyurMTHjITY3qOIEL+pgBGXqBKGdkGVYvWFA55t4nksgURRuomQB68vn0mD0zOFWk/yAgEXoFMHkqYtHyYCmZxBXyUIg9KVV4J/CfaG97gboCeeR/idECg6HZ1EzA0ruMW3bsAzroM0ZwXt/KwwqcD3YfXLx0PUzhlGkLdS4TBevVERPqRS18eeszz46iYGFx3LGLFxRtagjAQkiP2MfX4Wnztd2rutvRnWvQNraIWyORKK4H9MTZK2f2HNkDwrLdGepT49/GbRtvl99ytjsFBgE0ljPgxMkf/vCHWovWHrcrg0qqscCQJ1wCLLff9e3vfgvwwfbByy6psY6fPYaA0dFhxX8KDZrKJptETePeGYcXyfVguomrkdeIzhkGrOVSjQQHO/fteFjzEFQNAFahxgJSxqJmDVPOADhUIWXVAD4WYhc1U0ZkkOLkReNMjcgvpoI4n23PKlZXcpxhMJK0t3KpImAFpYNTpWISBChxErCICSZpelDLulZ5C9je2uzKL8KB1z9QVEj6PpimtJWbdpRhHRL1zSEau/696YvmLlg9fsKcB5W1AKuuhW8L5lf67jt6At2D70ycPfy1CaDKk/BNfX/RzIXL0E9nThuUTs34yXMWLF8raZOCoU//86cFwtaenh448o3Chs99/nPwlkKxEM4HYgFcuVg6dvbaWRwY7YZ/+8G/ZbJpePf3qu6NmzAOJyvrK98c9ybsHYjZf3zf+Enjic0qBRZY+dGPf9QoasCBP9r+fz71f4rB+vff/3uqI4nAE0mQYa8NI8FgV8cXvvgFnUmH/0JiESOqwBnoMwAEjHrBesABa1ka0Wh3J/5ThBd4pCVlLgALScWCUhVMCfsABGgDgYUncsNAkk0ISFmqlBE5p3tCXuBd4SoCoBZTSwFSUqajQdnrSA0MlqK3S0oK1ZVXV3Cqon0RYmu7iF3a1eLi4VeSWkuRpWA0XMBPaNOD5y5eqbyf73bM6suKl4eb261IndEsQ6Qj4cq4zEmDI95W8LC796vkDqKrdHGS9rUnHUhuaaKkNBbritrc1p/94qdIZX32c5/97TO/cQdcMI5w4TUmDfynH//ix3947hmr05rqSmW6MjXN1d/93ne//s2vwyziAren23G9z14++8UvfxHR4pyFczq6csg+lAQrhFHZseBvf//bv//Hv58y473Z82drTZoCsCZNm/TCSy989RtfffqHT0cTUXObyeF2tIfan3vhuW9++xsTJ0+saq5MdpN1m/01Vj+w8OZhcGfNn/m5z31OZpDCM0OnHioWUC3cS4Xk00BXUWDiF5wpr3pUWVOhCsq5YMHRYamScUoxNLMFqprUTdVN1dCCVc1VCBrEdrHML+UiBbcJaYjHINV7UNjdGSLqUMZxtnhMigFRpNAjEAdaseSO0lMg5ZI6sVvC/lrGXIB8+tuCVZoZI5uoRH9Ib8ZSK/NJBDY+K8evnatUV/FMfICljWgAEytMy7amPeclqameLvjL0BDBjgCNhHEAJYQrRxRYNoiULNL90Y7ohx9Cu+GhnW2JttZ25HlV8Y44NZfEbcf4g6QWXjMJMDviAAuJBhynOpOaiNyf8dLXhJHCU0j82OkpMIX4KxacwIFzRV2YClnbUqs36dPZNE4iqvDlPAiOwt0h8sb+3JPoidPXcWVxtzjwVnHen2u3pEw0CccU1EwDXTNJgKSXVaUuLV/H4+tbhBZ+i7a5RdOMhdoomAi0LcqgXOwQ4QyG3jTJm/lWPsnm+8XquLIgrVVeXY4VAxBkI6XtEnqMmkeflmKM1+OoQoM1bolChiRMSUrNZDTYM/DNIfjvmDUQqpJUFUsZrcaUzoDni8Hq1jYhi9SlhzcOnTsDqiDVmhq1D4uP+6hCeKLp1bHkVo6QkiddBcQKCpHonKGKFzc92z3RIGoAUlyR+CWK/nczRNIuAlhUpB4hwIJoo0omv1BYaOL6WCTRn9RREZmE6LJSeGTsGdxF+dplVkcLOP1F02f1EqQ6iz5EeEXqhKokWLJgXo3hS0A3ooIkhPKcsd0m3KegHVLqEYusQgDXIG2o4dfQ0gqOhRYBijBYigLyeJpmvpFPYeonvbpKFZMjj0C7ggcROOnSIIwseZOs1ZMS06ng2sfe8yT7gJPMYn/D0ERPwCpZBqYCApDjZ6k6fevSkQtnH8rKCVW6Gtq0zlVXSHvQdybrjYoLGiVQNGVtOd4xt9pdxasqAAuCj8SlCulgiVeoMdfaW28Fqk6nru3tOLeNih0TEIrACncF4d2bHKZYdxS/Yv00S1KLslnmkrK/mnO0dNNHFdBB97CUMQ19BETlJO+M6C/F1DRTatYC0tYj/Alg0Ssk75/Lpl8IJQwqarBrjwDNLxW3tfKNPFSE6kR1lEWkylgthS9fxfDKUkUVlZwp9Spjg5lCEscxD9AyrODmx30FGlinUEpMvJR+CrbBc8hUEbtXhgkFAxTstFKvmEXq5NWLGz7YQxUVpUrB+BYIj7lg0S9a2etjktaR/mBxFS/eN3ser4agBhAXgEUjbUNAbDfWhGvOxu8cTV3fn7m8m4pfeNPe1hxsvpK5stvW1lIMFhoiwBb0lqPDRs5kUVE2EpISukZpI/QBpcpE1RVWE6X6tBTtMy5woaB+SPaSWeIBYUhS0qaxYqHfABI8tPObXUkwuDopKU3KRhTyJD4xpYqBu/ctMVGhovd9SilYj8syaMlKDX1xK5i637osNZYuM1NA9E9AVY6svi9j1RXpw8/qSpq/83ev8klFjFelyYMlbhfn71SmkY0VijyTO87XHArAQhwLHYBrw6VKFpSCKiqNqoYGVZ9NjD84mbxxIHVtHxWbtbFfZTqppmKIq7qObwgoHz6+CyNNwEJ6GmBhkB/XDpr6G0EKlqK/vwJK8M6p98naRwPz7RdLP6c+8RjXfkB/KCqXB+UFto/rwOEdKiIyjjZSFAeDtJ6dL2yXQopNz6qZXNTHEbpWr6yXqryiQ+qlwPzVaRok8Nzb+ACrwVTfZG0W+8TcN02izbzbrlMn6EeV9ap9eclFPuj66DOOYTlLFStifrW4rtzVcI9FhxVqpGxpM+I1RGqZ7gwiRJS0O0Pubp3E04mZqKaSSGFooipug5MOkqobalgjmDeFpGtPwwWLLuQqdevn9RPpXaHfQEmwUppen4xM9ejXFJTSDIBRCTWDNHoBVTjzRGiCJH1G09e+nBuklT4PFi/gqnIFPgJV6LfTM18jAxbTfEhFF1Vzo7+HwgpQBRE6BQBL2CbAMaaLFN4QTD+amrkdmcpAb9khoSzub2bw1UM76smkHqIYisGS2vjiqoeShsoisDS+nLuTCQ8H+tfzYTf6sagvj2YeMisMzmJMf0zcAWlyBYCR2NxaABakwGGnYCmKIiz6KQhYyCLGFNS06XqNY1/Q0+uv6Iuw4+a3CpJhJcCC8aVaKsQop7B8IGNKPbzikwW9XMYsmwOjHfQ6phCppfNIoNJOKTqZzXO6H+uhD0RVHizSbESpSmi4uuryoxuUKgi0lMglavW04niQO0bJoQpCVkeBHtJTT/qbjdnSplqP/s+wTIfWQoQzQfSgkiRy9P7RJlEjRvvDdRUYeFiAZU+bkSgn6PT01NfXz50790c/+hGySp/85CfxE8fz5s1raGjAX5k+mS5Xto1DiS4Plt2IkJDLE+O3GmkQwwWLZJyJL1iYZqRdtfoEmjMlVKhhyq8pYhSqpi9aLOF+aRJPYBBJOYWC9TjnjHbeFpzE91/cJ2gcYEUGVaXKuEYc0ktCA1k6uAG0I575opjbhtgr8vT8VyfzysrYi62NqoR2AUvVlaqbUFGAiWWLFRXjHukZ9a5n7j8t800pInJuAxoBKzskpw8ekg5gcSR156ieqaoi4yoyC4CX2Wuk+aSKioqnn376+9//1spFr9RcGmmofC3EH4afOF7z/vPf/+43UA6qrKyk2ivQ0U4/qiaupWDVSFsLkeo/IYIVuNt9SyTC0v5+j4Klqpi8Aj+hBFhP6mbFaEsCysnyQewd69j1w3oIzfLcxlQN5+lU6TAZTR090Cc1pXIxfSJ0CJC/RfG7jCbgEdxyLeDVqlvUWxc4SD8hE+JCkUhpBZvxKtQlG9VZ76q3pFUin4buDiPT/N4HVqIfWG7F/aDoNl3lwahWjdFjJPnPrq6ZM2d+/etfvnBwZEL08kBybv/Ir33tS7Nnz8bjmRpzgKk8aEDVI7mG61QVd9/3abiYktvJThd1cc1Tf7AGsk3KAu+KXKSk/iNEheS+pbF2iCTHZczSYhmz3l9KUp3S/CVg4gkN02xDlSj5DjOaoVBFU0X5XCP9TxPKwTEqkCZVY6OykXZVlKGlEJSxSF2rvHnm5gVKFUTiay35IfWp0nkv0knYX2klbh+k4tZW2N08e1CqD8sJQBEZFJUJ90Rap48quGAFmy8bYwoWLGvaDF0FSl5++eUXnv+Fu+HVQaii4qof/vyzP0MTH2ULverKgAz5Rp6G15u1KjGGhFXvsGWo1XCp4mqsAnVFTCHJiahxYfTM3cVYBE2Ba0UWrfQb64COKJkqrnhC1aXAO0HNuECodqFLmyjQTAezqtCCpwcDiz5XnU9iq1hABxF4mQI9r4nJ6LZoWyhSebC4iur0tYuHTp9sNDRRqtB3q41pMXGqLdaGGkuBOS8p3K5Zs6bcI77jiKgDghuUrfSVPZDkncM+0U2Tm19g/qjoY0oWKUa01K+CrnrhuV9EhS8/lioqeORzz/58zpw5jL/VjakyPHVLPmtVargNXAL2++KuPmWoKkhlSblUaeNkiQcZztk7wqRQkTPTROlzC7QgU5uTPbZUXNiPECSgk0kITJ6TDSbYOJTaEzZ5WxB56Ep5XWw8ARzzGiuhpAXmgQS3X7OiqQ7rCA08LlIQkVtYhgFzmnYtBQtL9K1BB8QSsDtCTlfMQ8Xks06aNlfoEtlCDog91AbBYCdz3IQ1aI5EmyvhaQu7bUGnMWqmVLkEN9zSBzZ0xJPxw0ZbAlVqQ1tE5bXUB/nXCWFX96Rv7A/VnnNL7lotdfqg2Nd6q11ypz9ViAE91K/62le/7G54ZYhU9eqtV7/ylS9WV1eTzgi/GyMMmU1NSk8iYcMZ7vwFkiCNyIoamPJgwackq7Q5Cy7Ym8GQ6lcT4z5X2l8XFpexHyNMoF3yrrakjSSzwBrflHagFyEROtRqmrje4K+fbkv0PWZgpIhrgflhwFoZkxdQ1eoVonZZtmTFxtnzV9aq6jZs37nn0DEgBbAwARGNoJiHTsG6Vf7wl798+fWRkyhSkIMnTy9fswWt8a6Y1x5yYX7NlBmLT506bRLVQtLX9jrDaopUSQFwtoBEF5LZzXXQZPG7h3zyBwVIUUFmAVEe/PEzu157IqqonN038gc/+AFeARVllJDh3zBTD4zFi2Z7ffx+SgXqpEQpkFmDqWL62tgJXmTQecGbJxdYw3jTKurFUyeGq3tUH8nZ0g+Qjy20vFHSCsEWpJ80YiAaawCqyJfgEdcKapRhGVNTkgicAmZ6npwN7MoMbvOI0VPXb91zv6Z62swl96oqqNLC8vNmWevVe/coW+MmzBFqxGjewsr8nfsPY4VFRVM99gKxttslOsXkaQv5zU1GYQ2oMjvEgyBFhKwh1tMPP7jYM1YoG2QWfvCDbw+ETkT+blA9My4qrcziwmHf+7dvNjY24nXQcDHQhDd2BBcc4X4Oe6kInw37mYQQJ6hksjh5waA5xrOWEzef6QuI9k84JVQfzYVH4DZYx0CyX0mNCmmcZ9b1f5T/jkmgcMBSUUB5umZ0aqiYNB6z/lGFccP5lF6K3EJlWJmDNlGeUkxWb+4/guHYoErrNB46fm7Ttv2//8MIdJYCrLHjZwvUBCy0wI8YOfH6g/t3qyrfHDujgd8EthYsWbt05UZQZbEpuAyZyB5uuj5JMa3ZQ5ZgJ2kJRL4KmYViaGLikXL5kSWrFo8eM2rOwunSiokl2Vo6Z9j8+fNJj013cqDJNjTnju+lYE0zsSaJAfIIKU0pQPvePL7rgo4UNXORekPdJ77M+cWrqULNxAaeXKcK4bmCUSe6lIqJKjQF+pKtdnNvGERyZLAM1LazFfGdwMyn94Mm0S9ahFpCRZy+PhVqgsEuVdUELJlWNW78LAz0AVibdx6AUROq5KPfnNIsFTW2CrAbgFAtdYRdb70zC+tzMOW8mtcAsLZv271/78HKmmpQRaWyqR67BOw7dpwixVRItPonwahY0HkHIJD5RI6qmJjyyg++8IUvbNq0qaqqateuXV/84hfuHn2u+GFV50f85Cc/Ybq1OgcCC5qsIHElZcJAdll2AV7MOl5DSUALPkL+GqNmGu8XlGifUGMxmYsSYQGdO0LHunAfrGf+WiB5DnpDUTL8rTfaRdMOSvLIRWPpc2VjZYO8AW2J5KdFUPxmGmT1Up+EpQp98QX/ERAv0zqM6EWGew6wtu4+QAf+TZ2xpE7QgnUTv/nda9SLnz57ebNcgG3cm2QCgCWuqnArJBavlQULIlLLlq/dQql6LDR0YdPgj0GbOYBAVh35zwJcwvJ3v/Xtb9XW1rKVHKVS+fnP/lOY91LBI/WVr33+82ShGHGzBh7HBbsGf5w7NKZ/8KvRcNgyZvUlpxDSXIN24M/1RIqKtEb2Mt0X+j0mg6/KK48iIb2p1Dj2NgJJe+MGZDHrxfUQuUdqTGKunQpD0WFhcL5F11xRV8FKdXOV0CRgkVL1VrT6kEoTPxUny8BTq1ZOt5m4W10JkRiU2BBmwZL1kJ0HjlAf68yVa2+8NW3+YuyBEd66Yy+WrWIE5aFTpylSW3btA4voeVe7EQYa9OnHUAU7QrsbBn8YrQli+QNy6wW41N9453vf+x7+iiZmVUhFO0h/8dPvlh/5ZcEj8VzUfCh82GWvINtOhgZ0YK8YnTKSd9sxIEnM9Lfg++JOwtH3BfOagRw1XH7SPhqSDvzB1UN3olmrhyfSnZL66apSOk+fLkVVRkOXB3KXjtF+GxWzCKdR1ojEARbWUo+FzDtJEb8lP3UipsEAPRg+6N0CpHrtoIpVVOzJMopUsWA3AHhUbMYBc+IxMTZhUEHsVFGFrNjxgho+lK7rVA302DgErxz+BwErrhz8YTSD9alPfcpRV5gUFd99Hcv2c7kcq7Gy2eynP/UPypvPFDzSXvfapz/9afoY0uuHSg7Z2ZBs4GvszcKziqo1IMZsAnLMZB37D8PR0FipZHIVFTS8YeQmSN06KhvscyX7Kb9BRJcsEf2ROeRJ1UCOdkldRcGiGTh1VM3cGHRLSxMIaNY0o4mIrFrjeMM010pnmRiZfiyEvdQPKwEW48bpWFNLwcK7xzJZgmSRGKNGIOWItmHNJH6NVp0NtFzXxzVUTCl9ccRnzuiH4jyR1UWkDKIqthR6pnOQtCRkdXC36VKwkj7WyNd/N3HiRPBEqZow/u3RL36phI91YRS8NNrMTiZaFTU1yMPy1nasRZEIfSJQxfcKmPRSHiwTZ6g/oxJKjE5l4kEmwmcWUemTA35wUs5ihh8NNbNQVBEiijMxaIdMSbaYqkBBGxbq+sgUoIm+X4BFZzMxugoaiAxFosN2c3p9f3T6wGKALsAOw22xqEiJARIl2cJkWGfMiQO37EG76JYxJGfBgpByWz+qHm8EKT1MM5qCep1MXKOjDWL0WuavVocBZT4AMWXKlA3LS0SFzvpXwBb01m9+85t//vQ/jX7pG566PxY/bN3i4VOnTiWL8bvTxVRBKExcIXlRksFScfaD0TJhUV/iqsAIDuV2ys9vQr0/qRp6tF/aZx/4FVjDxJXisWx0jEcxWGSBAkdRcYW0qpcCi3aS9T+pzIPF0/FQ65G1S0vi5Rfc4PLECvKNLFWYzzTEWI809zFgaXt9rLxxyRmpB8OMIcVgOw0GqQGI+/fv//znTw+Ux5Lcff320ZeLLSArP/vJ9x88eIDXCXeEiqlCFhHdsAVsMZ67ip0sT7NuNDNZOmvPLEB6zAfvVXUwmk8UD+oGSIcOovZoJokrxVMkKVUFYCG5gEWnzMQoXQFVDFh5jUUa/5l2+5KQkWUmSDdg9hAxhUE5xt82yZsw4IrlSeVvtasetWkqSlJFwEroOGCZhgiWOp4Hi63U9s7fVhckprHQG1455h9BLemr3/gImXddxUg6QQlgtWXshZ19MTUzgVfKFXWkt0GPtXrQMUyKyJwrXQ6SejGASvbYD04aD7E2+okTDSpDSjvEVkEuWLi6TBscbZnSFGgsLlgKv4z4VUjqBslEeC5MzNxh4vYwg331QIou1iDtwdECpBRcZVmG/xVgUbaQS8UyN0qVsl3sEd308a8bwsoBwUpxwMqYhwgWdbBUbLEZKpcps6ARnm3XZBeYBzLEGk6fPn3xnGEfAaxFs1+eMWMGdcIqairwJZLlMREVFBU6sAuQgigjval2jDLkFH/IsoKkaqBUBfraMCfosR8cuko3tPR3ga3U9s+2PxZNHZOl5D6MKSdrODO6VVyw8BMvywxowMI1KXINGGtDd+Ls2/Gglycqyqisn5Yq0pFltNeFsgV5UPmAguWUP/S1XLM4eVySLAmLO+WxJix5Hyvd52NhiyJPyoeF50MAizpY/WwHzojJkkhmCWW4bykwnDxgIZfL//mfP+1rfrIitK9p+Kc+9U9SKRnyEeoIIH6GYM4OaSzDcB+/bBCqTP0zVUwwUVpd4V6XeKDnmBIe+S90g2nrocWDJGLN6LgPZiNE/cClZa7LX9w2w4574FIFqWutxURJLjTMvOr+2x30tnDKmDmPut5CBc016ErFCmVMQ6OaBcvhs0HkWrnWpDJ4jdj0EVuDGGN6Q0zniLXZI47Ld265k14MlUeAWhASVvIawFbBNwstWggWR12pvWpX0kVqCHROAdNAwqy+7ROswQcZb7/99uY1o54IrE2rR6GFi0mN9qDyT8FiykrkJ26MYqpQtMeVMz9ucwOuQFcBLKgEa8rqJbNSAraUbcC08BDjQRIlkJH33G4Wli3NwIqKtXckW9t7jY29k8DJTGuqewYGi1mxp+TqKjqrN79KlDV2jIIoGShQK8zsV9gLFoaT2H1Wo8Mwb8EqKvWiJsDEFWNch73E7UkbZnKySKFa7Ep7cB6CXdTYYs7iNetwBlvD2zMWT4cHP6lyYnPT4ybO0nqIM3v27rV9J06zGWHwRIbKMU6iKWoi4xtdri984V8dtUNVWm21wz796U+JRCI8N9IZ6qMqqTlw9vjMucuwlxBLFfZBwe6vkPw+UFmLI+lAO5AjaccxxlJAgfdpr4QJ57FjD7ObgRZUQWDjxC7pe9MWYTu/eUvWGBh8iV+VNJPhF2kjt8gzpOxoLw3AglVd2t4tqwbKe7FjI/uo6l06wdYK6ZphKmJvK9q+GzEFlxmNBICYRhrinBXuKkWmdskHzmXkMxp5ZZlgFlMQB4tp0IOq0Pk1Xrum8uZ1UHX45GkKU6up1eI2Yz9tHOsiagoQkqLqkBLpWtz69AwrrrhHqBPXauqGv/YuPYOZn5Ut9ZXNDY6AB2LxOcUGObYcf/GlsUYfGdvSYuOXS6p6P7mc2+yLN4Y5DuBj+fLlMyb/cYhgzRr3zKuvvkrTV1x1Va+tnzBpLvYJx3RMbVCvCWqMfsutB4/uVVQ9qqn1pQKQgvEnmjb9Cy+OkdhlFCxMeqrG2CN+I9mSqV0uwgITwpZk0ar1co9CbJHrQjpnwoX9CnVeA9mNNmq3JWzFYNHJVZakZaCMFGe5n4Gzxp9Zcj1YEqufu0MX9hVUoMmSMozFahOijINZbYxp01Ck6ICuEpv/kOYz9cA80TxIvnwJpVCmiav6DRvxiyMagUVYB7AUVhUFa8KUORMmz4Glw/HkqQsJOjHv3CWrMHR04Yp1S1ZvoPRg91t6sGHr7oZW3tHT5//08ttIseIMtibDCNPj5y/dr6oGWJhDCbCwazLAwoD1wZdxki7NsBaVPgx3/OY3vyG8/fjwkHdz5D/+4z/abGRgX7gzyFIFQQEf3UHYGZorAqUUbB05cU5qUNvDLpD33vSFGMd689EDgGVqtwKsVjNmnRGwBIbWh7U1GKh58vIlS8Lc0NYEb1fslm3fdwhb1VGxxq0GtxEb2R85c8YUsBh8piOXThGqosrFazfAGJ25cXnbrkMbd+zxYHvi0r0MqHP3yxFQPUR3Umbz7HRtDBP6lb7qVP0UhNtkyoFfjHoz2rVb7aI+pJIYRUmmCtCqOTtI3MDY04EUVb8kSFqDBWrQBWXsXBEm30/KLB1nt9l9FoBlDdkwExEwvTF2anlzLYVsynQCFlprsHV7Fb8Bx1PfX0x5wh6v9GDdpp2NYv7569fBjcFjoScB1v4jp/YeOvGorn7R8vUAq1bYBPKEajQMDWYgMBoEltGf9oOS8vLyn/z4+4M3KMcEL//sZz9Yu3Yt7WiAyeaCZY4ZlVbNrDkrtu48wGVL02bae/jkyQtX5GYNqqLNciGQmrNwNZ0k/cc/vdVobGKtIcDae+T44VOnARbUFXI/LWbe1t37WbDQz62wagBWRXMdxnorHZr35ywFWOj2HjFqst3vhoIEWBdv3MIrDPTBB9mskC7rMwx8sUnVuchioq+BbJnBjMJqdYpAVV1rXR9VcQ3ZXRBN9CGxlixC1jKbUmmYpVb5BOngVKmiqhY3j3pjZXQkkorZV0gXlrrk96GxzD7b/IWrrUEbfFLsiYUvGsd57TV5Li4DiokACz+xl+ZrIybC9nmSPjyMMrR01WaAdbviEcCCxdG7LYtWbMW2wacvXgVYkFMXLgMsyKuvT5AYFZjpPVDlq2//9Iwec8/AypgxYzauGKybdO3S4d/61rcyGeLyJ9WNXKpYwfCWSn7Nqg3b8VmwVRPaOtZv2bl972GwJdGo3hw7nc4knz57GbWGw14dz7fzkYiqENa8O2nug9qa/cdOHTxxioKFkLBGXvfB7gMsWLa4DSM2AZbBY3YkCFgTJ88FWE2mRoBl9TnPX7l5/f59rVuviaoHXic4IFjGQZGiiqrfEoxgP4cdImoTAix0yFCqqNHUMSN0yAzLouwoXd1aRFXfBYL3wvcI+sCioyOpgCqn6iFp3Qw5AJbWRUaHA6YV67bgJ4bPElM4bSEYAkkz5iyjPI19ZyZ+MjxtomChCQJgwRGZPG0BYwd1U95fgTI2peri9ZvTZy6rF7fUtbaMGz/TGXMVJ2ZKljK8OTdYiUajX/vaV5uujy5JVcvNUf/wD/9AffZc3B97cLwkWBCxS4QNc8C91KiaMHkeNqwHVRCtzTx2/PuUp6W9sw7ffHu60CWArsJkFDRt2wIuaKz9x0+i6x9gKUJkRQP2HgdSDaoWlV/lSrqxn8razduxhb0hYYA1nDVv+V3e/Uct1QDr8s17c+av0bj1OI85q4NkDQbaxalU0UZTpMDUzPhJdX7VaymwaCypS9Gaj/5xoiupsfAi2LECSFGhcUAZS5VXctthqDYwZRZ30nPm8lV8bAqW0q6BB4pjR8Rx4eYNSo/MnPfiW7UyelAjaKIHZ65cXbZ6M/aprxfz6JkZc1dg53psY7f/yEncxOu27F69fjvm596rqbQlrKW+09Ktt4lu4sWjs+873/56e1NhhOhrHP7Nb35169atdCW0p01QEqwP9u9bsX7LniPHWDt46NS5dZt3Hz17Ed1Bdr9nx/7DlKdaQRM9OH/j5vY9h3G3XLp9Z8few6vWb8dnOXzqjCVulnqQD5OThhmPdPqsJWhu277nkDeFO80nUEsQPuNPxpC5RtiI5QUzsDnnqo3L12xduW7b2i07JCaFOWgdaFgIU5MoAVaxBWQnRxpLMcftvyOeNaO9sDg5DxaBUjcEqvJ16CKDq2KRogL/jDT6UaqszmZuusWcNCFZBaQoWNaozRAj89Mw/ceatLYlnSCP+e76BYMI190pryvpgeCg3596N3YvELJTV9F3WnIRFRXUuTG2D9xgXdebo/9QANboEb9F4oousfem7ZaoPP7gWAFVlrQBitYSdBT471RsIafDj4YOJ4QbGNojbeyGHVyBKYSWUrQTjYWmbyQpPOSDE6pwgO+KzjHUxXTGoBk3aknRxjUDLZkfCljcbQoHB4tNQSEB3qBoAFjMONoSADETqfKajPpYqICRdRNBcbEIfUK+hyfwClr9ItLcQROkoKqdd61EftyvqBfVochTsPUPV5jt3TXMbFZN8V8RK5FFar2CbBnWtxQIu1qBmQBIptENNGuKFUxkIPMjOzt/+ctfblo5vK+LYdkwuFaxWIypNwfNKO3FlYn7x0raQSjgPpjCbZagzRA0av06hUdZXlOhDWmRMsDGO4zn3mYIG3AGggPs+YuTxrBJE9KgBgWwqHC/E7RMIegp/kLgTiHCxTS2IaeydFzzl9+UkDnop66yLFj6kh3JVFfR7TnprwIDn3juojqWJCRFuZn3EkMAg9KSVHGFm+gqa1c/Km78AASNEgzDbIRbOghYgwsNjFlBarsYLDnTlEgWj2PdAXPmsQub4BRi+i3ocTgcTz31mdsnR4CqWydG4NhiIbuVYIquGUv7GbAy1/eVBMsasfeqqDZlu0ruk0OU7UqZTVXZUEUxskfJPF9D2Eh/ZUUT1pIZw6hsBhTYjrtO3MwFa8BvI65mAnBJ8fgaMpeBrG+mq8oUnKhQT3fpHTz6Y4NH08DqStXbJENm7Br5SIqKrWQKPDOEUU37TfK7vpFEaN88EnXv+yFr+YdMFRAqYzt0uWxhHovQwP84VOHrY3pE81Qx63cljxX08AxOFRkbHCCjV1KdKTCEdV2f+9xnrxwZ+dRT/wzHixSbu7PWhIZSZY4pchd2FFOFLY0pVYgHyUHIaQnZobdwLDOqsUcoBcgWcdypecTyVCOrRzaVHitDyDHK9T4jwKKCGcaIdRwJB/Ly7JdgTBiQ/8RPHCNnyB1N0y9dydn4nlZL86U9EoiRZpXBTRv1u02DGkFUPzENEMkO9ItCV4nMIu4QwJIDuihqLOh9DIUKkcI+A32qkZkQg8tUZszHtGQ1QZ4qs3CI9JD/G9MpyFphFefWxLInGV3jy4Il61VIrOAM83QZ9yQ05aC6SkFm9zKiDCowTRkknT9/Ho0xR44eoVOTNWF1nio4jmFpx5kPCnVV1AaAlBaNRK9YuHw9JcwcsNeJeBK9UsPRTOaIVWQXf7D/AP11zoLVjqAb7naluLZe0Wx0W1iqbME22E2FSX27shziIJvXESlvqH9YVwvxJv3sknzqqjNTelR0DWDBuntKni6leby3NGShto8KCjik7eBJJlZCG1GG5Ew7P9VB5qiZXROfp4pjo8qYmjwJK/IFh5CyurGqJEZ0f1Tkk+jYeGX/pehk35+IvGDqAVVaJY0go5+Y7cjIwk5SA9bE84WwkhNHkP+Fy8mCJSGND3oM06bN7MjLY/S3MWrAS+kJVcQU2vytXSc3c6kyJXWwfcfPXURh4PDpcy+/Ms7UTjg7fv7iqnUfQO41lXOtHvJP6zbvLBdWHzh1slnc6o0Ftu44OGfBmnmL1p25ch1ImX3210dNId5YxDV7/qqbjx5iZ83z129QsFp1ikf1dScvXb5465YqwKyzCKCqiM2b+6o0BUtkJcwSGs0AxV0qWEsDe4IdCQaHifROMRtQYg2g2NOKRYIYMIQCDtbePOmSRgzupmAh4gMG1yvuqj1aStXxixd1sT4LSI0+1l+UFbZ3RlVVDVX4Gy55IVvYmSNHdRvJb8EYF9xqxcJsX6uSFqkrKghJiJWMqpAgpZjTMYrMwMxC1UUWUjL71KPmjYYq+JIQsBWOhrHnYLIzgQ3W0U/cW4VQmuJKu1fQdWKTCV22HLaAFBKh+HnuGikMCDRStcOAyjF2g3bHfOMnz1MH1Fy29HbLmLHTF63YiHdbx+fPmrtK77JIDOpZc1cCLCT8Ro2ZRsGau3CNRKvYd/jk+3OWq2w6ylaDiEf3FzIHbFKyAYmK3bsBpWVFkbqi0Yx2gGURUDy4OphuQgX7d0D4eh4rZFeBXuHpW0CV0CEsMb67V+RFg7u1zLz0fuOTSB05b/XgaeFaNGpbsBshqDL5LMfPXnTG3O6E1+S3kgF9LrEpYDaHLGV0SQZnuZ8aGzcShRRRlNRbuKVaPDwIzytAWmwQqpj93FUlwaI9ffSvZPxVWk+HnLDrYUiw2Zt0wDvEGg1uf07fDthheXVLNeoSkqI9c8GW3dkMsCxhWQFYm3fsx89b5aQwIDNjwrUQCdIFi9dBjpw+5035K+urWAFYW7fv58nw+irsUbV81Vaz1Way2pas2AR15Qg733zrfQrWxg/2yg3qVqVs78ETUo3S6LScOn8Zeu7KzTsnL1xqj/vbOuzsSEWaEy4cltR7mekcon7porgK3HCDIdbHELWJMFMZX0KLgZBEPHS/yugztChaqhqrBqGKTF/uH0nQMTV0ck6vp0UWtRLBUC5iZBR4APbUXL5mc4tMhM8FpAhVPuvGbXvmLFgFOXnpEgHLzPT9cMFqtZLNUlChIzqN8hQnzjhZz+kXU6rybHn45GSwH090GSTXc4fyUzC7ukPtk6k9EQX3q2GcPCOrsbgCs0t2pO7f9UVbsylYaNIHWDxjC8r1THmBKuT8IhN9UNJxZqvVLeSChd3z1m/eBbCqWhreGDsNlhFTKia+N19p06P8DKpcCS9ek317AGvR0g0UrCaxaOq0JW0RjzPsmTh5Hm5ZgIU14hQscAmwsH+i0+kOhkKBIKpgDmyxiQOY7PYA9nHJkV3QyVx1IymzhKTFtyIcLGymgjETzpydCxaowkrlYrCU4X5jGvAKKJCg9hWOhyO5MCBrUNQXwKQNIQHudiWdypCCu8SeW+0o2d7DPgDkKZ3qaTMXoyYGqtDKUSmrRs0eW9fCr1i3ZVezTJQfFcmChXuUqitmIxeygSLxkBikqPC8fJFfRIfv4K99HzKmKv7YQxEWLC0HLNLsUbiwjOygYeyNDSlYrQ4RSvTYkEMZVhjypat+PbXJ2wf1ITl4ajHwWkw8EhIGbacvX12zccfaTTtuVT6izvvd6opV67dtRH/CmbMiQyvufvbtYTP2I2fOU7CwBRoKoMtWb12xbtuDumpQBRn37hwKFlTg0tWb44nkt7797d/97nevjXj9jy/+6Y9/fHHNuvXwAseOe1tlVLZ3+HC7DqLmtVEt2vyx8zQW6+YXFjMzzBvljZCSXyBMlZxxcOnl8GbcCr1i2Ihh2N+FOFjKhgKwormI0W70R9p9WV/JYZPsCCRGragKHwAfMa1X2nXzFq1tQGE+RdYAy9olW3ft9+eC3nT7tj2HYQTKyII4zpotgEUm1vkl3B5OVlRRRcnPRhZlp8gWOh8BLKY9rR9YpEDBRSpbuHcGCxY2n2nRN5M2sqSqH1i9fWqZa3t1ARlTGRQL7SLS3ZAw2iJtxTl3k98G7YUDeCd4WfbtmRNm9LgiLCD3QNQItgwuE7wrShVk1oJVFCyILdDW3dP9g6d/4Gtvz3V0YMNfIp05nKRBRrQjgt14ct1ZhLSINtCn5c/46f48yc4kEvegAU9h9qL6EDtko5GabB0YVGBvqWxHNt2dRqkK8w2xZRWqIKS1JmVKdCVwBhLvjJmSBlRN6FZF8DvRfIsXxC7i+L8gwUwwlA3hT8vWLH1Q8wD/qTvj5Cy81hQMqc+vgE3lV9iSbaFjpP4IXYA6BMBSkfuZ2Srh/7V23tFx3dedxx85e7Ln2E6cZO2TOFknWZ+NU1ySnNjePUmcxF47LpIsW5IlSxZlNaqQlChKokiRosReJYpF7CTYSYAVIIneMQUYYHrvDYPpvQAkld3v7903D2/evAEobXjuwRm8GQzAmc/c3/3d3733G51Y+c4mgOVLB5974XWMJKoJ3secY9B7lUWK0zdrRJWJhthauVzDpzAhyOMHiNX6qnqxewILqwDCQdRAchM4zWLjntZgyegqZ3dxYJll06TwsnB1EgNY0Hyf/w9G4yvqF0CVQjcOnnxZP95OentQqPP1b34tlo55Yp7RcaU5inmKRgyj37Rjkz/CutmeX/r8jr3bv/LVrxw4foA1t/W0f+Nvv/GlP/nShm0bsITB2fzRf/8jUp4GEM6cAy+7NqBNZ9OPPPZIa1sr5ND+/lt/D/lZPKe36MGPoCYDqn3QTuvo6wBG06VpOCSoQLIZmfHwG6vfONN6Bg/46l991WBj6rV//Kd//Nv/9bc///nP79i9HYPyhWJo2SH1fKTLtcCcunHlSGsLPWC6Ej106tTcUVvRcWOgFw573aadOrcZRQlzYKEHqGekh00u4DCqd1r8HMF6sPKUlfmUVElCK1agLVkEK3YpWFkWY4151QiGsA4iUBBTZa26K2tCgwSpLTYpCxb6gOupgmm84wu6WHyQEICTx/Lk3PibhXJhAisSmxJ6/1kH28e3H37sYbPbhG9x46cP/CQUDUFWE/Kff/GXfwGXBi+y+r3Vh5oPMXmzSp6mmOAHcQqEvg+VSZVIJ0Bbx0DHzJ2KzWf97Oc+my/nURy7ffe2FategzfCs/37T/793OVzeKpxw/hDjz2EZ/CEPRA7VmtVSMeMqEaAF5t28fHs8reWw2ORpJlsOYm4+pkfj12wtKm6Oyf76QH+ij9QCYiF4nC4bI+48DHDjWAhPAfWiGEYSzu66UPFKey9YeiMsKXsQErtU6usaoPfYkosgAgSzeHSlKfg/tRgsSKkWrB8ea+jaKstKGNgQbEIYCnd2CvNBVi2ans401GOjgEse1RTTxUq32t4SurVLig396gd6vn/VGpoJrD4tbJkrwerSfQvnU8RTwJYGuM4umdRO+oOuaHJCHlsdvp5d7ZQKeDkIF/JASwke2kdVNgUkOrEk5BWo6vgwEq6YvVrnYOdcFe/9Vu/BcK8RXeg6HP6HV/+8y/jMWKwIDIKQO1ZG2S5OYXHGQh5Qn4RYAlU1YNlvreagEaT32BN1SkdJiA1YhjBV0fKcaj59ItL30Ipn9ZqmvTqcR15ap3TDNP7zXJ4sZ0jYr2pcqRXPWwNO9xybIXLYRjCvepHn/+L3VmPLFgIElEAjQ8B5kf4sj6sOKzkl4HFAizssQEWK671qniqiiZxxsEVUjCwpsYZTMhZgAZu+BubWZ02CFTp4zrMp1O5VPMjxVSZy7YqVTZOQI/7X+RZ2S6MAhECayoZFjwWPASsFizNVHpqeGJ49vasWqP+0Y9/BL917vJZhGKxSozAgpNj0VVKD/k7SFCnC2kCC/E44qS9h/c2n2+GTjYuwkWxrqy0qThTZMrCH98Rg/W1b34NT2XLWil0w1cEXm+tWSkFK2ek8f8Urdu4MW7UyG+5Z7DEkT4PFtvApwz44OLreGgc1XwYjsVIspvAFhKY2Bb1jo7giivi9cXD+qROHVQP2ofsGYivBoUsV6gUQvUcAyvlrdn6cWGvQq/pUYz0jA772MFcAL8UcRL6+zwJHwqbhL8PPT+WlMmetKktmscXLRFCY5gt7EQ6jiJ3TWAMYJFhq9IXjpwxFUejbgEsj28IYHmKFtGIeZtk4ZuMTGI6OVQCTRnDQmBxbYP8wBKbEFBywFnIQLYAlptTyuwd7cHHAPGQGKxx3Vg4HQ6lQ7fvzN6+ww6mSuXSqnWrNr+/GZSIwcLrLAEL8sz52zmIot/qu3n34zu4CMF2aJGi5DCSjPze7/8eVr0FwVr5thSs+l7qe/dS/MjT2mrNJhrwYkwzsAR77IkXh8bUiExBkiWMsSTm9v6u3XuOomgdTQQA61dPvPTor17AKMDTrS3hXAQVMgQWqkpeWPIWwLKE7B8cPYDXXeVTP/fSCiKpa6T/8Kkz6zbuQj04ZiQtevrVp55Zjpap05dbWYWqMP0378Lzo/9CZZx44MGnCKnj585v2bEHpy4793ykdKuILaVd0TXSe3nce2jiDtnhydsj0x5+KZwaK1w9INYuAA1iqsYD40P6QYVl1NhgX6JlPQX3sJSXzQJbWMK+/s2vC2ANTQwOjg3Alzz06ENmFwProV8+BC1gThQ43j3Y9dKrL2KpAk9nW84se2XZ3bt388X8Zz7zGXgv+DlrxorHAKwUtxR6pzzAAmLpn/udzyWycTC0+r1ViMzwDHBUazes2X90P3Jm84NVmCms37r+7KWzNM/i/58tWc2pJmCEExLiCQsEAmFH2AP74KMji19cSe6KDJDtO3zio6MnAdbjv34ZOUYghU4KfMU6aOYKsLALff7FN0GVyW9B4x5edGVA9fRzywksGAYwA45LN9tRRwqwMJ4ZF19Y+hbbxlf/0F5D/9r12wAW2n6QHHdG3JaAA6cuyGSqTdrfPLfcNu0Uku+Xu0ePjuUFsGDHdRV92oL6/TtHNvr8o3P6F5iuVkUKkwRwGDKoHVRzYzYbEYOT/Dq/VR0ePOexTAJV2D1JPBZbsvXD+ULuwQcfnNSynuwlr7yMPR3AwgPufHz7nU1rv/rXX/27b/3dd7/33VQuGYwH4cMgrg6n5fK7UICKgRpKkzKZSwILeLW//Ye//d3P/+6lG5dmP56Zmglh7hwicfzGP/sff4adIHKw0fL0PGBVZirJ2YTdb8fJ/eKli+FKZRuEqPryXhbBRk2OTWKk+P5uDiyyH/3k16zTIWJe+c7G9Zve3/nhwQPHTwOsJ59axh3ph9FJEUyHCCzElagshQcyBaw6l+mZ55fj77up7GRJ6lTA4DW/u3nnug07ANaGre+bQ3aAhYQHBse//OpqBlZxDqw1723Tuy2YLcjAmvYMT6hw6oLECYYM4txNDNbVftXpEZ8YLNglexIB1t1D6z0FUsZmB+cCVcaFVj3BJhhYbOoBtwjapNJA0BAtsGwhUUVzGbK3s9QdhF5W4VVOVZLkVKirG/4JUtNwn/6St3K3DMcD/4QHpMppVCDGikw2e2Z2BhGYJYPTWxOiQAKLRWx3Zu/cvYMYH5skpBvClRBSDPhxaJ7lcWCasbhyLpYk+w+kvMp3OfUhXIfvwI+wsqLZijNvRz6MUl/Z2Sya2llQlbfi2QIlf7AUlBiUQeSreUsOf9FP5iv4JFIuTcZ0zehcMVi2gPu+B54CWBh9++bbG9BusPfQ8cPNZwDWU8+8guYINk35ySX4auWWQvxlqMcFWDqPyT7lhKO6MtqOPipQCG5wKoeTE9a78t62rTv3eJJ+nNqSG1u7cbvEY2GIPLp6dE7Tb557FesgPBZ+Fv082NDCLAmzAFb3xNjl/slDE7clbNkCYzMndzhYWYSZomCyYe3QgjxRKZKWnRnPpwrJi1kUTdXgvWF3JBwPMvhi8xRcVlZuygw5W4wvYH2tgmV94Klf1a/2qPHHoy4cCVJ+Vxh3u5Me7GPQcU7myXvMabOOjXLUIWeG3yXcJRhowxs9EdBA/pOu0J9Bt+EXAEc9UoKh0BwP85a8dtHESn/RFy5PBYtBUEV41QTvMsU3KcPLy1av3bjj2RdWYvkDWKBh5dpNr72xnlrhANbWD/ZjBYRhpjJbCrkAC+OsDD4LwEIIBVBOtlxc9Jtla97dqjCME0Dv7zu4aduHu/cfQS8hvp2w6+k6mg5Q0zP3aUjZj509996WXZt27rne1UEx1uWOm2+v27px254z1y4JVLEQPqRtH1S1dw+eVoTnIq2J256Oy073sDii0kW1o+ZR1LgtCJaW6wvnanmNNHuoVpXPJtkBoQhTnHGoN7z3eBexorXcbB23a3A7UAwKYLmzbsA06dbeHL6lcWhwG6WI7FQtNI7mT1YNkDWXKqWdH+yEjzl67hjZuevnu1U9zoSL4IDj4ZO3eQe+NQSMF9ounLl6dsQwim/x6+D8MKxV59Db/C6Dz3jiYjPWaEZYKQywUJ4PgFCo2Dvee77tQnPryVOXTp27dg63L3deGbNpwlxDQ7AYdpb41A+ois7EuhU959svmKYYmo6qqKyjaJcBy53zIDEP9wukHCkWuTtTbm8i5IkFsfYRTxLjI/ei9/j58yvf3uyOeYkY4caC5q0N3vnOBdF+ECe+aFfHZhMmBott1koWhW28vWdEAOuCKua7edHsVwlU4bh6RDeiMCkaRVRcG1y13pWKJ3Msordzm0pux8cKLhq5JRqP0WjINjrAAFPrzdaTradOXz6DnUpQBBZqDwFT661LuLflZosAFsyRceJrx/itcd24zWUfUA4KYJEdv3hiwjUJPvzFAAtIqmC1dlw6dv44mTFowhVMCoLTsoZttoATYOH6yUunpsrT0zMx/CBODgAW5m8LPyWxGwM38R6R64LfAl6garocxd8MRq8PtAEsJ1cryuZpVWxNVTk8aT3hmE0thO2NrLOni27wlbgF26RDB6EKncvgiDjdaKRO2k0Jsz6BQl4tk6BJ8FWU+oREWpKfASQefy2WoKFp6cPBERj22wwpbmQNKwwvs7w8pqW1dQ0KYPUMmQGWaWpcAEtlU6JqY544nZ24Vyt5pMx9kiFp9VMVUGKFbQ3AOnXpNN4GuAG0J+HtEcCi5Q/AsXvbzuM2UOPBSjs7NJ3E0Nnr59Q2NSpS2KqU9WmcGlxhbJ0/YQqZGTpc7pCWQvgqgYkrXVcFp2VJWu1BF4EFi1biBBbDsRS81HmZrveM9VrCFkfMofVobw3footnrp7Te41giw6eI+VIKB8GVbBLXZd9yNBWrLyVbU28wmByouYTlnC4Ij7nlBclR46ksxFYOr9+3DvGbnNgmRIGjW2STDGpkmiviU1Xq1EjGYwu+55hmiOoGgmNEFWifDFb9ZW+8bauAQGsiZ4BgGVIaIGU2qVSWkcXXP7EHmseqrgigk9Q1Auq8HHCWw6wwA3sQttFRKuBghQswu789Qu4jZDLEXKkZ9MI9pHiQkNbucKOk4t3SsmZJBYvPnJKuk9fOQO2Lt5oIaflyNtwki0BC2bwGxiOeR8yXv5cQABrGnOXOLBwpA2w6KfOXjsXLATFpraO0eOPXzjhSXm5Vc+KLkB3wk1gXe664it6BbBwCtckGSrPIgaMHoj4jp46h3TDyfOtuI0RWYgf53FdWODxAum8eiCFBl+WJc/5e0d7YTgnHrWNSsCiuHh+sPiRXdUlbyI2DrDGp8dYKWl2bvUhD9fW2X9Ckzs2UT6suX1EM+vtuASwbPZRzJFTmEdxCN8IJiax3CCJRSfZEqqYu03x7hYFjPXaYLJGkTKBdbG9BWBhfl0VLNYM7El7CaxLHZezM1mh5Fr2H3aRydkkMAIrCrOK/Jner2eoYbhS3lUP1qXOK8QifkoMFqqppkrTAAvvIMA6dfk0LiKuIp7AKPN/3O0bQzfpR1BWBKTIzGELgdXS0eorEVicKlHO1MS6r9JaiRrR0tfeRoc4Tlhd057FL72Jdk0Y9nqYDDOgZZPmiSfc6BjrRXMBfQuqxiwaaJ+Yg1ZP1itu6B4yDonBmqxTlbaJ2OIHO6flTVA7EnK+EzFtv1bZrMmCLdhAZ89kd6dvsN0WUDbkiY2o49XhhErXhs5MgIzpUGqrkl1GSiHey1opBgtLHvruwVawEHLn3BhSArCsERvuGlINIcVwz6LXd+G98LN4swFWp6KT7RALLjeiJRFYp1tP0w2dz8CzJQLLlwE0bFeIfT3AOtlyChebW04GGTdQ1DZzvUY2fKv16OhHupRd9WBhBfdzS6GFV3Y1yQTvnboe1MExqkrsAP/djbvYti4dePzJl1esWr/45ZXb9u8FUijN2XviKFIGy998972t75u8dtiNnr5rnZ2HT5xBja8uNDfgC+cSOMymmhzuQ681y+mt0zEAtdqxFSfDOSeR3zKm9eLUMPbk6pBGGRyHtQ8oL4+YWyaiAAtmauClyOtIKL/HtJagEc8tweZ7Vw6n7T2BBetSdIdyYbAFY6F60tOp6DJYDOSQSPT6G1//y7Wv/+zGkftM7fclRn9q7/pF37lH3n79/q997ati0evsbA4OBmCdwfqFxQ4D8bjgXQDLFLISLlit+OxDIURgYd+H4XIElo0DC1sB+il/zg+whJM6uC53yk13Xeu9JgsWxVi8QIssWKfbLxJYYJ/AQtmkJ+l74MHfQClz0q5/7sU3+pSKQbUaVGkdxmBmCvlMAgs2qp/Yc/AYDK8XP39iSq0IKMhUYRVWXq6k3Sz7WZd5n8Rg1RT56wgpspv9w2Sgaswi76sMdVQZk6ZAOhLPZcO5aRQ/NVgNpcvinOBWwSz9bMjFiBKwyJAvOHPlLFZAGFHFi15/5c9Ofzi/6PXDYtFrVF/RauhOezB80FO7FGLcgdKspttar57YmnBot+/Zd/BUM6gSeyxh6cTmQAwWylXgtAg7ZDHqwbp4swWBFwYgkEoZ3t/5PBYbWFD1WOag7f6fPeVN+PEhe2P1hn6Fckit/mD/kWgpDtuw5QOiCiI8+FmMzQBYY/4xptFQMA+ZBwWwFEEFr91QMNcO2DQ12qvP+araRO7E9KQErFsDw4oB5q4ab/345U8X1zlTnqlsIpHLi82XCyy4HxRXmFBbC428pq41WfU2vEn1YAmGFZCowuCJ7//bt+5R9PqH3/+OIHrdO9IHsCxTFmz9GAS1YAXzYXg1LtK6zLaHhaDOowdYR86dFsCyccH7HFjpGrDYnrEQxJ9KoX09WJe7rwAs5Oi5eQtGebBgv3j4eYh+gSqQtGTFGhZguY0AC9XyKA58/qU3rGGnUjfx3qb3p/LTMIzAI7AwawUPII9lmDYyPcj45BxVAaiXTxBVMGveVC3YMEskbkWzwgxcXCWzEcPWUhXm18FezeCATqWNak3cAmqW2+KBS1r43Gl/PJeTIAVL5vKpQiFWSlHVr3neif6s5qdoqRc7lR1fi6VEAha8FJKlV3uu3Ry8RXEVfNX3//VbadW9i17f94Oq6DU0hfCElK+SpBtoRsuwboR3Wh6tF7kMn3Hnvo8udVwXwMI5txgs/IgYLDsXZmEHyoK2y2fqwbrWf11IvvNgCfnibtPgpeGbSDdjUzo4pl793pYd+/Z/cOQgS5mmPeSx4KveRldnXzdQQ0/65c6ba97bumb9Voz2Q+mLLehc/vq7SI4faT6LFlDk05i7sg4LVEH2GTAJYNGqxw8xLzaOguft2cWMCaV1hHgSzGfvsciV5wMsXzpcjxQshYPi8kyes2y5jJeVDtHkZsiaGp3/WxuMJvQWfGKwesf6MI6aIvr87UJV9PpPMZD3U4teYzG1xexcvipQD5Yn5aNvWzsuW8NWgPXBgUMtNy8LYDlqwZquxECSCCwrvj3R0szSqi2n6sFqG2hnJ4ZFXw1YtAZ1aPv2nj6jDKnNWemrg7NxkLRyzSb7tJt2iGQY9ueMuRzRmouCUaPzoG4ANZAI203VFZBfB6sLH5X7mT+VlK3armZUJWuossQ1hasfWev2ehiGliuXE/mCrK8SqBKbbDEJN6vTVK9EAu/FyfUyQ9sMKvGFNnYvd1ZIVCHNiH4pfIsjNswGQu6ARK+P7fg0Cgmn9/6SRK9Rb4N1UDgclIAVKob6xvvpyoRjEmBhKTzQPBdjUYJUDJZkKfSkPHTXiQvN9WDB9ZLH4jKaTO6wyZiVrC9sHz5iHJn012QEcMiAYhieJ7iiNJrZ2YgYMqSt8YP6FFd1A4ErzggszI7qHu0WI8VRZarNVpv0Gd2CEqPW2qZt5P0ljoosrG0rX/hQ4rGIKlhSzl1liqV6qnKlSjAbwcSLWg3BBkLfXJMnt+Wkga5acUSIk7i5PNaNFvJVFqSOygESvf7mN/6q4aDeC/ed//An2kvfbyR6/Td//T9J9BrZh3qwcP5BV8DBxRsXcaVjuIPAwmqITCyB5cqzIx2KojgcIyzdwE2nQrV7oOBHop/uahtoQxEEju/EYCHwIrDcRbeNC7OQIKVPmJadd2Yw+RSJ+EmVR8VGR4hzmDinTHsJLDbHp4qU2PAMQEQCFmzMrUY/uMY3jtQ8ZnczkbqEVjIXWWa4T90CJJoDYIDO55hHRSRhgKo+hekPfICV6j5ZbN1fUxJdiBBV2VJJdh2UdVfpYjmezSPAt2Qt80smiesnWZdfXdZUDBY8FoGFPyxzO02i1++88aDsSveLB777xS9+8bvf/e4XvvDfHv7pX4X7ZKaRr3jxh7zo9Z2SBCzs48Q1DshtEhwnW08ePHX0wMkTyHv78n42uKDoAlg4I+LzWzjN5FJZcKvBAuALXuu9ztxVS7MjzqploJSJA2l/IYCQi9gyR8zEFtZipH+bJmOTNO2JDME1uv3RWIFWBdkSMFPWJEsVm+2UYW9/PVjIF4yahjkRqCF4L7K5WVBcTG1YCCxBXgvOFiXqoyac7eiAkTDoF3gRWNn2w8XWfXzvA+QOcnF4qWS+kC4UZd0VFkdZdxXPFQAWLJpN23N2WVmb+VW4ZMFCCExg4Trq7Ej0uu+czIzxh372L88++yyJfeLr079Z9MiP/rz+YW1Hfkqi16g4lYCFY2a+tKbEtorIWp3iAvDq2d8ZCzUrwGsU3QCrffAGvyvM+MRHOsN6PvbHKTUeBneF1yFQDqLAoVvVS2DdGu1AdI5VnuogmgbG+2GDGCQ/CVVgdhvDSdRu1AXIv9O6tK4eKdY4z0SnDSRjKQULC2JMN+Zmc6dwG8hilhzb9pN0bIZ1WhsWXgp5DTpDXIdnQFEyYcRNgmDTR4SlMNNxHEuhc0oNqmK5jAAQcz+JdCiemk5mJAFWPVWJKlVksWzOk/PWq57eo/KgOMbCZpAVQnFCL3c4bWJk1R1dv6iTkMUouS8SVcl0ktj6b3/wOf3lf5U80nzzfqFdDOMqxWDhK32LM2NfkZ0hDkzO1S/g4A8FgAQWWAExavtY1aWdah+4gbCsd6y35VYrX+AweIPtDEpBtMXiP+4s2gEWOz6/wjstnEcNaYe1bgOOXppwQKtyKhRsVskoOlUmpyYk6aK6KWFaCVI6OpflALJx7Qb1YImNzUWxKvisN7lJDjKwZawd/CV77AMPgT8VzcrksYCUUUQVbFp5CWDFh85P55JigCKpdCiWhCWyeQlG2WJZ+BbxVryWKvxsulDKlsr4KlQj3aOv4gv9imxXiGQS3rC2/naqsMN1RO6NRK9vnHrsH//xHzk/dBsOg9j6wb/9w6UPvtVI9BrPhl0FhfBEg5Bt5wpd7LjLm/Odb7/Ig3XxhD6iJbBwIMPqsYp+ASOxAcE+TT8VA6Lcj1OrY+YvBcCWKWgWFkTBmj7hRsxIPLF5odzMPgGpKliCx7LIUoXzYFAlgCWxmpGkSW0jp2XJGsdcfIyFldFOe4I8U87FFWdgBGDB0qkkUEhVt4HRdAa+CldqqCpXcIVxUyzDElKkipkqc9kieyTeGwqqKFpnO6ACP+58Hs5QfQWYDH5j52gX9NhZoV8hgOt3uUG9EL0O9EtjrMHz95OaOrFFN773T39TD5a//+ckes0aXPNGLxAphvQB462RTkPQyFbAUoCrAbF6Sx6qicDBIlCA73Rn3AQWuSKYO+3GoY2AFB7WpepGXEX3okiYE8LgwcJ/mdWXlqdcCTxnt4AX6gTvFSzUtgZzYVaOnJsiUzjUMuhQlE3ZTjmw0L0uCxbqHcQH4WyGWx1YJpHqPRbEgfEBOxMhMwsGl8aSGmldYvQiwEIdNs9EqQI/BDIkSx6up4slAgsmRiqVL4ofL/CXKhbgfijlVh/IiydL8fqxfBWvX1Ka7OIEKSofV0j0uj7GSo7++Pd/73NqtVooarBbdb/z2f/i7/6B5JE9Zx8h0euZjyvss4ecU21dMpY5AgsLojglIdQlj7vHvFwFqWDOpNM8ZbEjMVa9AiiRHBGQErEFIVw/2IKhnsed8rhSrEaoid5FCnHMnK5avcY6/hqUn1/r6ti++yO0ZBFYt0Z7rVlLvTVaAckmfJM6r8EScpiiVjY3EWWAcb01a6s9wzHqWP5CpiJFAEtpUmh8Y2KqyJjHyltTquvFW6cLqu683HavuuSVBaTIsEQm88VMoZSrC7kkjxTQqR8qIXhWSd+BmCq8i/QM6dspXvR6lYyq2ZEN3wZb+/fvhx7CsYM7vvTFz2xd/tf1D1u/+gESvc7cznDC1UbkM9luTih4L5GsNTNX0SkBC8qgw5PDVESK4nJJtbuv5EOPDHf+ZpzHEG/hkcFyiAiDNem4AhItt/+ndpR6vUZb2HX/g7/hxqP7uhWDgtOSseyUJGUVqyQEw1xGk8/uiHjInn7+dQiAYbYsGdp+XDEf3Q5no4KFstOolmZ/RtLujvpo7CeOw4kk1JdNl6I0EwAWzAXj6RRZIpWo8U/FSipXZLc5BwY+wFCmhDREJdcAPtxVz1++XIGGnqx8hkluFiH6iasDpE20vZ0rpykHSfT62//wDdk01fW93/7e//7Sn/zRZ//p737/9Na/l33Md779DRK9jt6OsHmI9OvoBJMSUewQk1Fl4UqrpYbwxjspbA/JrCyuYEMbawEyzI8X2zIX+WnhTYi+KYJmrwinsgez1h7boaHviSeXjurHxQwZvBbhNhpv1qyHbVeYx4mnTm0X3cDZDrq4QNWpixdXvr3h9bfePdXSAqqOnj33y8cW//yhZyx+O8G0Zde+V157Z+/hZqvXqbWZMMjryq1bm7bvwczPC1fa0OEIqjCUdtOOPSveXL//SLM5bcBSiFaiXz+1dNnyNZhidf7KdULqQuv1TAYH9MFzLW0ef5i8zqGj5zCPr39InSnwVIGbeVxarjSTqUWKrDQ7W7p9W9BSq01oVScScr2HjgYHoHNZsZwZ/REkem3v/TSi1/aeRwTRa/9ttyCBWYMOpwwtg5TYsgYxWKBKV+1TImJY0R4TmpyPKjSC0zB6pkwRzkTIBKrsNN1A/BIErFt27X1j1XqT3ybAtGrtJubD4t6HH30esZfKMAGRmTUbtxJPK95ZSze27d7f2n4jWooZPTbYsEb11HOvksd6YtGyto5Od9SL7sItuz7s6O/rHRrFwE+dDR39E6+8tralre1GXw+aGTfv3AeqrEHnD/79sY6BgV7FyJO/eUWhnRwz6v75nx+0+9xWjwvNPweOngJVkXjs8V8vyWVzweDUzt2HDx87H55O/PDfH/eFI6Fo7Iknl3iDkTTPVlkWKfi2WCYXSaQFi6dz/I8US6AKNu/M8PrjzrpvqxZNxEj0euWr938KsCCVTaLX5f8ocVTZ2PTrMtf3UTAuAFOt9Qx39ykxqVvFBjimmEgYAyvLwMK9Om7++wLuqmQhsPD4JnTKL1m2uk8xWgNWbVERBk6YglYsiC8tWb1yzUYCC2fSGKOAgYuPPf4SwIJhYPWb69YTTw/8YhEP1of7oT8zqtOAqp7hYZxP3/fAk6AKcnOPP7kUIyFwe0Ct+P7/eeTarVvtXd3Qb7re1e0IeQBWe08X6lFhS5evIbDwsCs3b93s7X3ksRdOnL54rPk8gWX3undDAerAUYBldbrue2ARwIpOxwAWrPVqxw9++BgIO3Gqdev2/f3DasH9ZAp8UE8xViydTWTy2DyKqYKBszmPxYGFcXjzj1uRACQfcYY1WqOWRK+/8IU/iI7+7FOLXk/fDktlKVgwaqoql1jp/ZYsgkxlLsX6XDDpTxhHPaAbIKo4MQHmpdDWgQHMTNJ3PndFKlF8Y1xT39gIpifCcL49txQWZE7pqeIR7/HNgV6A9dAvnwNYXaODGEpDYK3buGPjrg+Ipx/d9ysqZNi57wDAgj317PIr7bfQx/xP330QMJkDjl/9ekm/UoHbmI0OYt7fewh9/RgZMl2IoVACYMFdEVhAVgDryPGzJ8+2YkIJagybz7T84IePQtpuuhg71HwGo7BjqWRoehqOLZ1Kg63de48BrK07PgJYe/adOHn2yqhqMi23wNXDFElkYqlsLJ3DJrFmKeTAauS0aqrKslLCkNdFE4BhiuVoEDX3qfswQhEZcxK93vreI59Q9PohEr3GXEl7qaF6hXOmKuFUttY7Kk4hZ1I85xym8igJLK6xkY31xtcF3BWTH5uLzpv6laNoh3fG3K60SwCrPjLAxFwWXEe9AOtQ8ym2FK7bQq5r+ZvrQNXo5NhvnlmuMKkJrMcWvdBvGxz1ji565hUCa9HTr9imXLeG+0EkLYUvLls9oFTAdWGFfeyJF+wBF5Aic4bdAGvHhx9d6+wAWJt27AVYnlgAYDmnvfhLBHv6+demctOTdiNG1gKscDQKtn56/yKfN3DzVu+y194BWHaXH2BZnF5ZpPhcQ+3aB86QcZB9ZHn2zjxOy9Sw+AfDfHQYF4hUS4+ip3ukp2uou3Ogq6OvMzIbJtHrL3/5T4IDP7tn0esHBNHr6GxErFTYSMIJxuq55dZBxE8kuzrHllcp+C0u2JovwMKsVBZaiYocm/DRx3i/lhvXcQwugEXbZuQghPT3W+9uwvyPZxavOHXpEqYtgKcu0fZw94EjuBcjYrrVgwTWiWtnFy958/mX3lyzYbtSr3FFfRu37Yac5KJnXm3v6yawOoYHoMut0I/Tyvjmmo2IwRFpASz3tA9gwV5/a/1Ly9YYvTaARU4Lz9B8sUXvsRBY13u6Fy9dhSnQb7+7ddfeQ/F0NpZMv7lq82O/XoJ5xhu2fKi32OPZnNZkQ1j26guvHz3YnEnjX6pnaFyttQi4pAqlSNVpTacaUsV5rDuC0/KUxXMNrCQT3wAsI04LeoZ76qeMwJFUPi6T6PWSxT++R7Beeu5HJHqN+Tb41YKqKpsH28BjMeUwrkIJERjXpGWzFufCfHwkELA3Ymv+LaGNC634+JKtmKYmzGO90NYGfQGmgFWwCpG7oVoBgqEAyI4SQCxHmuNzpGhWrtkkeiwA61p3x1wdX8boTDunikwTGktVpBC1hpxCrkHW7GF3JB8FWP5EEFSdbmsxJg04VgugNDELvQOT2FcJ5on7I/Hk7v0sxkrlCjB8O40zwVQaSIlNZ7QG/KGyZqikGWo+166aMIuJAUwI0hFppRtThVyDQJXYaeFN8sw6YTiVaqQm0jXYhUNSKVjcsT2Gb7MpWZzoteoeRK+VVx8WRK8Fd8VP68zLiEM7qmxR3hx/rRtaEJyJ2WIrY0Yndl0qn6oKVmN3Vd0MCmJSuNG0ctWm1956F1QJS6G1wM+2FxTIKX3KUupVaND07M47MeA1kGMCWmjUeePtDXuPHsfhtiSPJf4RahXs6OkQ+1ixYS3HCMZAKTCVnQZYk+FJ4kkwFEqgF0Ny0ZV1AqZh5fiwSkNgxVIZo8UqoUpsBZ1ynmVRYlku15XhzgqLs7fFYOVmS+j69cw6iCqY8H+pr9cAWJgfLp1oWj0Ey97JiESv75tf9Po73/4miV7PfDxDEt/CWCVrA7kUpkfHuSsnhlJXqXJXR/HUr4zwXiBME9VAl4D+O1QxwE3/1prmdogmEneVWJNtyokcvCPrEK+DQsEaX7aWYvkMcVVxjeUtw44RGGZWM2FIucew3UDGsDBYSS0NYsTxljmF18giBggukA00T+klYE3nEsSTYMlMTqlSB6ajjcBKxqKFidEGJBVRY8OV2ZRw4INjHy6DymdKkUotcnkswaAJIFAlBkvLqW8I/SA44MdIEtmhpgQWDolx2AdWHn300Y1rfj4PWO+t+pkgeh2Y8XG6qWJSG8jZc8ktjEwXUyUYN4bJLEMY4sKR7vp3ypTlQy7yT8xXlc2cUjAUU5g1ibMMQtjOREpEYJHOHYFlqp68fiLDeB2iCkV/KPZqBBYeIAz5JGODIaoA6ZLcEJi0vtZduSRUkflCU9OJ1DxOqzzQlkPZouRUJ4fSmhQsivnX3Mk0kEIJvHCkSJAVZmYEsDKz+UZgsQ96yqCNaEZMwyqnstFgX6EeZHp2ak70+pL8gqi88oggeg0nZ64bgd5Qt5cDi8k1yIEldmDMaBhYxYbVsGeop/6dooLeuRWwrumtCfM5tOEJiH45ilZhM8hWWRFYMK5qxcwf4OcNn4gqPK2x6q7wqcWw+UZgMZmTWrDYQQRbAZmvYmIqkGFayF0JZnO6QokEihrkV0OtotJ5MW83pdMZAawk4rMkgvc0bnCLIOerCmVhTYS7QpiVr8yInVbsdlwAayw6Lv4facKaYf0Q/tcYbSfpm+Wns1apIivczc8nej18/1f/4iskes2kK2o1bBcAi9sPCgA5RWFWQ8hKtiHtEAYhsS0tMl5JgSpj7cLHi81Q7TgnMm1u0gTHu/q7Ono7xLhp2LT3cUj8QoYJrxSBReNcebm9hWDiphTbHdTSWaVKYVHUr334i0mPDo5QQhUZ1pFJJu8JkRU2WExMlTVtbUQVmcFq7RsZsjndkaTUeyXS6bxeXem9Wh7qKFj0OZTyczDVRle0CFYjd4Gn2tUQlqxkQRU7d0vUfGwgwaVyqiRUmbmDf1mw3GLR64frRK8f+hdB9BqzEmRcYL4hWBau6kRY+IBCFSAZwjxMP9COahHorKhFZSxcd6exPqKSibFQKYrprgJYqF1RTauhviQY2GJg1ar5LAgWiWu6oGVSXQRhTPRBhJR4bIuFbYBtsobJwajiMtUiBePSwbr5wSIbn9BpJnSxTFbWdaUxb8xmrAzemOm8WNQqxXgBKW4prFS/LRdreWJ25w6sfOfOVGVK8rFRO9Xaefv3LaIWATJ4d7QqyIper199vyB6nZiJS3IWgoBDQ7Dyc5tBpJ2QlKfbbHJ92SJZDcnDwfCpAFuGlKjqLnfPYGGrIoCFs0YxVZBk0sR5j1XT17AQWC5O2RAmUAUOsC2q5nO10vmUdTw5uHWQtXzUxu/40HAnWfzZ+b2ApZnUT2iN0VRGzBbCc5ZfYCXL7NvUdDhnnigP3WBsiZwWBVUszKqiVqhI3VWZAwuGNhMxWF3DnSqXYgGNKhFVpNUIw/zZquj1F68fZ+32V489JBa9phlrQsJCHEg1Agv97wI6GEEI4zHiYqkasFh+y0SGw2kM/psIjgtgsXHUIoCE/IIMWNDeFMDCllIZUQlgqafHhBhLVCLMdnmym0Tr3FLIpsGyQd8cVdDtGOWEG7Gc1c+Xchdc3HlRPFwOYQHlB6nnzBIXpWcSapMSuxew3L6AzmAGWDB+HWSdORWufK8ocWAzF/eJwcoUqa6Gw6tUpjIbrIli11WushUs1Dit/on+jv6OhUZ5W0je1iIKw8FN+W6JOsP+8A+/ePHQI/iK21w16QybNQcQ8akrWBBmsUqVIhKk1kYS9qjvk6x0BESj0MpbcQtgwbSRCaVNKYDFqmCq9NCGlMs4yIGFLkJxjKWuLoVKJkfI5UiTWvEhj3BbvD2kklHmivN0hQ0bpnUQ+wO0P6Bhiz0gK3PicbK1Zd+R5q0f7LNH3IEsmsR96EYimJwFGxJgjKqkXoAJ7sqQMFhSVm8mcC9gwcxWmxisJOt7JlxQjswhxXUXZrmLpZYPI5FIPJFMQW6wmm6QWgUD08lmCTUsiLnZshisiZBmQbD4LgE2hLdmf4diPWq1OHHiBHolLly8wMYb/d+7gds+Xh+es0AuHMiKjVW1uwosJcn7kqIFiUZPyu8pusQ+yV1pHLZXHGKw8G4yPITq89Kcr5rrXJIFq5/NRhvRMhlFCDON81RFVLg9wQlbmrhRnHLNykbBRRlxWglh96wZ0z4lhtMxmn4m26ZxaeDqW2s2nbly+fy1q+64DwfbMAwgtQTtdLveULyFB+BGNJfkM6JovMnkGlGFRLzZ5gpEosIOUfBYMFQusEirUBSuJLLZqVAYFgoEg9MxebDqjO0WZyqhdAyRn8DWiGVkwelIfCF/Tpo4QIE5dVtQowSyXFN3QmKqYOjoAk9Hz55dt3H78pXvYriwQJgTlZ9wV5wMNonmYV4ISiahOjNVifBBesmJwTKYLIy7PBWnt+Lxl/kiUoyhr4JlMfn4wmCryDlZxKMGZMGC3I+QVgBM6qia7QRj1NE7QQcUQvuAtFSN90/mW123GpnKrmDNZHJU4bc8/Oiz9egcPnX61dfXfnTsBIZvR4vxZSvWvvTKakwc2Xv4eP/YKCYx79j9ER6WzDGYHJ7AqysgWXGAMJrQWwyYvEzRFc4IXd7WK7deX7nhleXvdPYOEViRRPLRX734+sr1Zy9eb7l8A8DBM11p6yREzl1sM5lsAGtwYPjZp5dt2viBlQ6wSwuwlS4Wp9MZTyoo9luYKohxlY1GUcqqi3FD5JkOVPC2H6qF6LnAV9yWUAXzlj3AqGO4/4lFSw42nyKqMAlBadLghrfkc6cZUp0jAwyspL9HMdwx1N81Moj2Y7Qy4yIZ+0iXQjCUoaOic8yqRcEBGQ5zoaxucFuCuYgYHQEGyrzT/CZhWWRgdQ11SrJWZHpRRy8NV+EOfExz0zuybDAuYialRSGpNDJzE/csWVOjo36es6xh8dI3mBPK+DF6hE3jTPl37T9wa7Dv4ImT0KfYtecgMpAP//I5FKyaAzbIXry1dnNbb+fWnXsPnTgtdkt6s6Pl8k3cGFFqDh87Rxe3bN8PJshU4/r77n/SE5pCCL/ijQ0Gi0Nvtu/56MQHe46SW/rpA08RH82nW291DZDT8rg8nd2DP7nvqVAif0T7Me4NZWZkqUIExhqBmKX5bW8a5fzMFaHvQ+VQ3qMILZv8PmuvZ6iR4VQNJeMQWHBMezx5xBJhhUGz/9jxZ19Y4Yh4iZv3PzpMN8at+s6hgXc378LYfWPQ8tQzr76wZOWyFWsw64Wo2nP0BPRmL7Rdgxg7gXW1s/PIqbMQUG292Q6O7dXz5rmhTrWc0eBkLsayDEuQ0iYmJCep4sJlJtPAAkZOhSvDdM9qkZqT9ZJ6uKz0hmZa88vHFwMsnFRaU1aYyjP2xKKXB8YVZOu37iawbGEH07B4ZTUErnED9RR7Dp0QqLra3vXh/hPf+/4jWBCT2Twqc3ARpVfX27uBlMXhab16a9fuI9/69o8DU9Hzre0YRg+wYEebLwCsRDYHMh565HlCZMv2fXBadrsTYKnHtW++ugZgDZpjbdo07o3kZuSpymQ5qjIpblV15p1CpS9S0COmkQHVALZX0jSBnHoo9j33Dpan5HXFfcteWwNuXDk3Oa0L169hGt71ni5cdEy7F7/8huCcIGeEu7bv+WjCrQNYavsEBsYuXroSYE249fuOHkekMWbWgktoghBbSuP4hwePNF+4EClPSwIsa9EozbxTdQMcNdcyKu+oaqaN1R7+gB4Cq2e4V6BK3BFVXy1orFsN+20D9z/4JAolILVAYGFe9IpV7wpgbdr+IcACKBjQBZ5efWPdgEaBG6h8RzkDUfX22m1Gq0M5pv32d37i9oVwBfV9V9t7Hn18KRZCo9W5Zt1Ovck6qtQArOlE+sKlG7/81WIxWJSvemLRUqJk+/sHAFZ3z/CbKzfgp+wuH8BS6n3pc4ewUMu6q1guB6TAFlZDuhIvpgWw9HEtDPv2Qc2gRPoQDMkpjX0Cp+WZcWJc2fMvvk7cGH1WtAsAHdi2D/bjyqTDiDGf2Bi13Ghb+toa/q7d+8ZdWtQ1gSd73Pny8tW4caXrJnoRUIeCCjmsrahGgZwR5Om3fbAPtXGITDDYnR04Ivaai9zNtlpVDtrqNlXbjnVUzqBL1QyZgWqcK+PlpHKkYJk5kWB4e9RACr5KpAhlksy7wkk25FuljWWZ4M8ffvqdDTuxhE84dcaoCTo5KO0CUrcGe9/bvGvd+h3p2Rzqp90xD3iCJDhECTCLa+v7+w6dOAuG4IEe+PlzKo2+q2/4Fw8/J/iwXy969YGfPRNNZk6duwQsrrZ3jyjHv/O/fjKdTIemE0xdzOLo6B5auWrz3gMnCYWnnn4VDm9gVPPsC6sAFhbWBx58xukJnD1/Hc8wbgqmzx+NXzuXEYX5wvk056tAFZf3KpXRJYu603A2bs1ZBbBAFVRMxG6JwGLpmCRrVahVB5Z1WnzaSUgTkrliHjQccMJEAaVxAn4I+rmgBy8SRe5PP798zKqDkAwehspb3LVz70FH0rVy7UZaAenGsF6NR7Kg6vzFZxa/Bl81MqleuvxtTyIIjwVDZ55MrFwXebMunZpx06IBCvaMM5CM0omsPzWFQgMJWKQ+jQMZEVgGltIsmOtjfDwzqBqPS8FCLyVwOXbuPErdX1v5XsdwH74FN2+v27xq7cbdBw5jbxjKRo6eOYPrsJFJFd1ovXWjf0QFgIDCrt1HN2zZvX7zB8MKvmwG5X7PLX7jrdXbKJZ/5rk3X1q69uWla7bt4AN8LJfbdh1cuuztt97ecujYOeLj+s3+Xzzy4sOPvvz6W1s6e0Z8/qkTJy8989wbMNxIpjF2KHf44NkbNwYlYGHtQ3RFKS7WoZ/Jxao2ncl4cj4CC5Lbfco+sbImd4qqrw5v1taG8KY5bmYdzhkbF9nMKRTvbT7dMdnv5ip20HUHkRhPgl/s9hw8umvfoYMnTsNX0ZXz16+a/TaUgO85dGzFyg079x8aNY+BJGPYQjE7FkR8xZp44OTJ11aug7ta/sY6WgfRDgMhtz2Hjh4+ecqQuqcx9+jjkOmEtkHbMhUhpARD564sWDCVTTEZ1szF47ITH5LMXWlqE61Vgm1MxSRgdcc9BM092vyJqxeXvKVQL3Dg4/KFsA42n7mU4opk4pksdgCRKFgtyJo9mDpy+OLJI+fSc2kw2gzyeVQQFhNRJVgoM01sQXh7rFbKFUdVrJcBdRC1Hovpa3IK2RKeBDtx+fL5jnaAhXESLK5CCJ93I+fnyrrdaeyBAkJQRcZSWSk/2wDGfKwfmuOpkbX1d725eoOwMUQjJ9NxTtrxdk9OTyjso5AvRV4dGWC1TyUvK1c7uc8eSEqRIgumphuBxWRqIMoX180DFkZGASxtUrvgdDVb7XQ1MsmuggyZQFlcQtHEqjVbNmzeHZqOwXXxPiyTFT8GK93GLXs2bPpw87Z9oWhcYsiLNmIrncmVbp4vmcZl4ncuJSY1rhEDd2GDArB00UmIKtxb0sEky5Ng1LDO9zYWrbKdHQ7WVm+dX3CQe4zNhYRWCbJywTH75JEz59Zu2G5Ea3HcZUqxnQe90WIb84wNaAZ6lb2o34fsI/SkpGA5wz6Fbez0lUtav04WKcFoseMm7LIWZHPWIPwatVOpwPgXbkCIEFpJBlMbsjpxFG+Q86hm0XQGWbDw+SaqUNuPlx5z+urBiqYgNWEx253K8XGLw+kLRQwWi+Qx8XQmHEsIJHmDIaRPhW9RJ9gILFhRp5q5dCiPHqA6ttjAIxFVcVHiHueSwVTEEDF0c5HDvWRN5weLJihVH2n5TzFWrVXweBJ+ymzV8yRrYy414YVuthqw2nt6Pzp6avFLb2EDNQ9Y8zw1Ii0cg1MaUDIjRF5hJsOGbOEzZ66Z6sZ6/CVI0bksFRtiXJFQ4kj34pNaDxa8FMAyWNhdU7EkTLgrzY1myHDl7XQF42VAwNDIyKhCabTaCK90Y6pg2XRm9vzeolHOaZVKsSpbSMNSTTNieepYhIXjCYC1IFVcnz4PUCPXJezwLQXTfxZYYhtzqiXvsonrhOYkGvnqLrqOXtZh7XDfeB8q+nEc3DPSo7SPMrBgHf2D6zfsbrvR24gqvDTQFZqf3MkpTfdgd+9Ir8quXJCtRs6p3vDCsXlJ1QMAiQXrnBaBBUok1/EG40RPMG7AHwML5g2FR5RK4DWiUIyqVIFIDBaOIgovpDI5f2iqf3BgYHjI7nQRWzOXj8xch4h8uZ4thPB4QjxztnovfhFRBQtMRYbVI/50eN5aGtII4qnifJI8W8L2/j/RaVkxIsqpYgMT695fW13RM/BiCwjeelJkqhoDa9Ji2rxtL6iCbdnKAg4Zqqonaxjlxo4FxRSLFkT2CyIaqCwDr46eToyRERiiTGAj8Zxa4xWXLdw455oESQMWpSeDiSTA0hpN/BXOOWVrqYJluGlYvLFRWMx12T3eCa2uZ3ioe3BgXKt1ev2gbXh0FJDpzebBkWHmsfLFGb9z9sK+fMBzL2eI+EVACn+VyWYfViqwBOP1DOeiGDjYyF0RWEIVDT8ms0GYRTlJyajBeYTBWa06qweUudeUMowYhlAxIOM4II4nKhMV2OIbexJUiTlXDdV06OiZYycvuv0hgIXdeCgWb0QVSzqXYiyDVYWJDZ6v5t8lBrAQ1LNgIm8Sr2sMGlbOZWrknOYbQ9rgpyKZucUumS3E0thfWVRjY0RVrpYnHBVjQcT1OaqqhpBIHMJbXW6NTgcOUICVwkDARGpMM+ENBLO5YuX23cr1o5VrR/Pp1PxUZYsod86FozEgBcNzzo2uzGV9uaA5xyf88K6w2t8USmr15IeQ3zLVnQJxqo5Vj5WXTojlhpPJEyaewCs73Au+ClShkHoswPSC699TaoVlf5KoThUXDVWPpeXmynKyX7om/9T0klfWrVy9efuugwari4MpiRcxw7bQfAmlYMlylt8VFriJZ9xtWbC6+rvRPkAz/hY0Xrk0a+DnN6e19a155rp10MzJ6UzEWIsSDxaoyiK+YUuhxcbOoTNcPbHE0pwPWxAsGBwVXorizCwMxz5mu0MzOQmnVZ69XTGNV64eLkCpvjFV8FKYGQ7rHexTjI/BHdaP1o3m0mjbNCSNsiMOZXaLRelS2CjS4D+HHGf1n1iuvcBKrRMEltqhQiG1hqtEZ7vvuveUnpO7YRJPiOBGuE9IrGkqHQ/FEsFp9hVUheOY0plAeloWLBRS0imh2BqBBaPus/mpMs/Nh9XPDeBPaRuN46bCL5ZX5JAicydZ6iHOUQXTQlSVQ40NcK8DCz4swxpyFgYL9TYCWLBAOKJQj7GNYXmmMnunMjFYVnU19FWlMpAKx4BmUnYGuNgcSXc9Vdx485rMljiQF0+mxGZIn9HKSnjw7iRZe/ibNwi90SRxjSqVm923xqt1ndSoXB+80ysvbl2Ei6mnCqcsTcAIWxVh4cO3sBRX9casVJK8XkzKvA4svATwNKi/08YmSQFV6VTyYOWM8wfmEqVJ5OglR4p8iX3Vt83Fc/FJASxYIsNTNQlHJgTsXAmeyCr56pV03WoYZzkw3lFNcUiRCWAhSaFQqwFWrlhiq+Htu2XtSMMSmkIRYAWjUVQXLggWygwDyWlT3CIWoRWqIWycLrU4usLRr5nmQWSrA8kTGolMrpCapjdbDBYrH62CRUO8MNQYYKHvWXxqbODHFuuFnCUfxYvid16hV0AqwY/sb0rkc+KIKpxIIpcouKv6+slIKVoPFjq6eItN0MTbifA4gWXmSpnlV0C2YZ53nDVrXhPHVfwehN/lJrVisFxJH4FlMJv56EpKVY2xKch1bFV9FfukyYKFPaPD6UL8TmAxG+2QAatYTCViACuEptlMNpHNzQ8WTq/p9fcmwsBLJ/JVrLUGBztVsPimUF7BSo/MM1FFVu+0tFyDk766AlAQhkWQ77ivrqcDE/0AS+lV1vSTcgsi/QhrS2RrC9+kSiaE7dUYC79FzwthSsDCajJHVbVUt3Y1LLmKDlmwMMxdz50lCauhUHbYKLS6R9GAao2lUZI8E4MFu9XZoTfxm0Q2YbsxVY32hnPrIAKDqtMSwEpksgALliuU5sCy6kt2o/glKhnHZy/sL7YcAFhqtTKOysXoNCydTCTTKVmwMCpc/C6Id4juGe64kAPLUR0DLpHgg5vXpWXiB5K4quaijeJWCzafTYjSsoaO/k4koqhmuNqlzC+IVvlmMiPuElOlF/12X8nfNHP7NstBx7EgJsVUce6qLOvkU6U8FkQKthxFG/OWcXMQYziQFawallcBLGE1hJNsJCoupBJqdi61rs5cm9qQgDViHhWieLiie6GKNolzS2GmLniPxdFOTWXHbH+XzY1PTDg8XmA1B1Y2Wxm+xUmqVCrjg7evN89MjlRQrRnwU+TeN9Dn97iysSis2HdFHqxEFaxkCvsPTKjHhtGeQyFD9RyaRUKSBKnpE41Sl7TeW0Q/jmlYNdV4CMgybHo+1+6gk/Ro4Hk4aZyaoEpcaoX5yLN3Z5s4Cca7rDUqnRVTlWlAlTiQ50uR0tkoZ2KwMDdBZzaG4lEcfOKYmYdDBIqVmzxhzMgkqMTtQFwQVvX8dVsEMVid/V0CWJg4eo9gcc0UPFj4L9SfG5od7GgI5g4E2TFirjCHVNUKqeSd5m13jm0uToUqwAux1+072DvarJbu/l6v34+9kGCIBevBYpMpkynW61F73TvjIbC4kR7SPJZ1XrbEH1FxixjlCARfJaYKXoqN8OOmKGi5MioceIh/ECsdNQ+LDU3q1d9ohq8CVYDq/wGJPKjhBlKIngAAAABJRU5ErkJggg==", - "description": "Allows configuring location of the selected entities on the OpenStreetMap.", - "descriptor": { - "type": "latest", - "sizeX": 8.5, - "sizeY": 6.5, - "resources": [], - "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7867521952070078,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7040053227577256,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"defaultZoomLevel\":5,\"provider\":\"openstreet-map\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\"},\"title\":\"Markers Placement - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"54c293c4-9ca6-e34f-dc6a-0271944c1c66\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"6beb7bed-dfd8-388d-b60c-82988ab52f06\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" - } - }, - { - "alias": "update_multiple_attributes", - "name": "Update Multiple Attributes", - "image": "data:image/png;base64,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", - "description": "Allows to create an input form and set values for multiple attributes simultaneously.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "\n", - "templateCss": ".tb-toast {\n min-width: 0;\n font-size: 14px !important;\n}", - "controllerScript": "self.onInit = function() {\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.multipleInputWidget.onDataUpdated();\r\n}\r\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-update-multiple-attributes-widget-settings", - "dataKeySettingsDirective": "tb-update-multiple-attributes-key-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "device_claiming_widget", - "name": "Device claiming widget", - "image": "data:image/png;base64,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", - "description": "Allows to claim the device using name and optional secret key.", - "descriptor": { - "type": "static", - "sizeX": 7.5, - "sizeY": 4.5, - "resources": [], - "templateHtml": "
\n
\n \n {{deviceLabel}}\n \n \n {{requiredErrorDevice}}\n \n \n \n {{secretKeyLabel}}\n \n \n {{requiredErrorSecretKey}}\n \n \n
\n
\n \n
\n
\n", - "templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n", - "controllerScript": "let $scope;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n}\n\nfunction init() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n let $translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n let deviceService = $scope.$injector.get(self.ctx.servicesMap.get('deviceService'));\n let settings = self.ctx.settings || {};\n \n $scope.toastTargetId = 'device-claiming-widget' + utils.guid();\n $scope.secretKeyField = settings.deviceSecret;\n $scope.showLabel = settings.showLabel;\n\n let titleTemplate = \"\";\n let successfulClaim = utils.customTranslation(settings.successfulClaimDevice, settings.successfulClaimDevice) || $translate.instant('widgets.input-widgets.claim-successful');\n let failedClaimDevice = utils.customTranslation(settings.failedClaimDevice, settings.failedClaimDevice) || $translate.instant('widgets.input-widgets.claim-failed');\n let deviceNotFound = utils.customTranslation(settings.deviceNotFound, settings.deviceNotFound) || $translate.instant('widgets.input-widgets.claim-not-found');\n \n if (settings.widgetTitle && settings.widgetTitle.length) {\n titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n titleTemplate = self.ctx.widgetConfig.title;\n }\n self.ctx.widgetTitle = titleTemplate;\n \n $scope.deviceLabel = utils.customTranslation(settings.deviceLabel, settings.deviceLabel) || $translate.instant('widgets.input-widgets.device-name');\n $scope.requiredErrorDevice= utils.customTranslation(settings.requiredErrorDevice, settings.requiredErrorDevice) || $translate.instant('widgets.input-widgets.device-name-required');\n \n $scope.secretKeyLabel = utils.customTranslation(settings.secretKeyLabel, settings.secretKeyLabel) || $translate.instant('widgets.input-widgets.secret-key');\n $scope.requiredErrorSecretKey= utils.customTranslation(settings.requiredErrorSecretKey, settings.requiredErrorSecretKey) || $translate.instant('widgets.input-widgets.secret-key-required');\n \n $scope.labelClaimButon = utils.customTranslation(settings.labelClaimButon, settings.labelClaimButon) || $translate.instant('widgets.input-widgets.claim-device');\n \n $scope.claimDeviceFormGroup = $scope.fb.group(\n {deviceName: ['', [$scope.validators.required]]}\n );\n if ($scope.secretKeyField) {\n $scope.claimDeviceFormGroup.addControl('deviceSecret', $scope.fb.control('', [$scope.validators.required]));\n }\n \n $scope.claim = function(claimDeviceForm) {\n $scope.loading = true;\n\n let deviceName = $scope.claimDeviceFormGroup.get('deviceName').value;\n let claimRequest = {};\n if ($scope.secretKeyField) {\n claimRequest.secretKey = $scope.claimDeviceFormGroup.get('deviceSecret').value;\n }\n deviceService.claimDevice(deviceName, claimRequest, { ignoreErrors: true }).subscribe(\n function (data) {\n successClaim(claimDeviceForm);\n self.ctx.detectChanges();\n },\n function (error) {\n $scope.loading = false;\n if(error.status == 404) {\n $scope.showErrorToast(deviceNotFound, 'bottom', 'left', $scope.toastTargetId);\n } else {\n let errorMessage = failedClaimDevice;\n if (error.status !== 400) {\n if (error.error && error.error.message) {\n errorMessage = error.error.message;\n }\n }\n $scope.showErrorToast(errorMessage, 'bottom', 'left', $scope.toastTargetId);\n } \n self.ctx.detectChanges();\n }\n );\n }\n\n function successClaim(claimDeviceForm) {\n let deviceObj = {\n deviceName: ''\n };\n if ($scope.secretKeyField) {\n deviceObj.deviceSecret = '';\n } \n claimDeviceForm.resetForm(); \n $scope.claimDeviceFormGroup.reset(deviceObj);\n $scope.loading = false;\n $scope.showSuccessToast(successfulClaim, 2000, 'bottom', 'left', $scope.toastTargetId);\n self.ctx.updateAliases();\n }\n \n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-device-claiming-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"deviceSecret\":true,\"showLabel\":true},\"title\":\"Device claiming widget\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "markers_placement_image_map", - "name": "Markers Placement - Image Map", - "image": "data:image/png;base64,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", - "description": "Allows configuring location of the selected entities on the image map.", - "descriptor": { - "type": "latest", - "sizeX": 8.5, - "sizeY": 6.5, - "resources": [], - "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"showTooltipAction\":\"click\",\"defaultCenterPosition\":\"0,0\",\"provider\":\"image-map\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\"},\"title\":\"Markers Placement - Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"c39f512a-21c6-6b06-3aa1-715262c6553d\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"94bf5ffd-b526-c6c3-ae3b-ab42191217d9\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" - } - }, - { - "alias": "update_integer_timeseries", - "name": "Update integer timeseries", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAANBSURBVHja7d3dS1NhHAfw/Sum1IUQBZYJIdSF4VVQIOSNXZiJg5nmWi18CXG9WJgSqd1IBUWu10PY1EBcIFYqyoTCLOZWA8/0uGmbbjs73y7mS+Z2YdjYs76/i3Oe88C5+PB73jhwnkeHiOxyCh4uOQxdxO1XIXiofndEJ/uRBuGXdS41HSCqS+dEWoSTEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYSQfwl59xkA+teqDfLWl9wOESC9twAo5QsJIdHI6EstkvoQX4UP6LsNzNo/hgGDrI2oCI0B0XG7DADuSzcttaMC9JG2t0DTMEZqezuvAwY5UhbEvB5a213J9A0Awg0mnwid/cM1zBpCmPQgWrGwARlv1GDvBABbd88TESDhc3Ov7wM/H99oKp/dgEgXWlqaGgFAWYl6hBh+H9rqp4COV1EYYpAA5vWwdXi9XkWoeeRLjVkDGofxqdwDg4zqrxjTY+b8HDwOoSDaZQnAeNXFVuMEDDIGKs1temCwpsE8JeLMHg2u95qVGHBR4xKFEEIIIeT/gyxvKYgB0d5sXhAtN0uxgtS8LBJEsxrtm2sko/TbTRSI9tzY92ddj1FavYgD0axbHYBklOLnI2Uh8fIRy0kCR8pBgvdcALRn8R3bhMjdrdYlAFh62mpd/Uk81J8ciGKp/56gXW27aQ3uPVqac9AJTOUePpOfMw1gom5/VpJSsnC1zpU4H9vp7KEDlSqUQ3qg6GQI4cKzwIvMU6eTBYFiMSXOx3aG38mM9wCMBUDDAIAr+YDzB9qTBoFiie/4uwmxuGJ1+CgqAYBkQhDcwSVKX+YIANi6SvKmkw7ZwUXjzL5GAEDlsT2l8wJD5o+UrO1o4S0sFhcSOH4isP7wYFdAVEikpEABgGBWF4D27KigEK06u9/hcDgUlOUNLQ7lmkRtWksZsXiEhaqsjN3mgMij1lqEPcJsY8OPD4QQQgghhOws5E5SgxkhhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCElNSNocEJweRzb7ZF04PQ7RVnXpcay5il+KVmW7YpZ2rQAAAABJRU5ErkJggg==", - "description": "Simple form to input new integer value for pre-defined timeseries key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n\n
\n \n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}\n", - "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n dataKeyOptional: true\n }\n}\n\nself.onDestroy = function() {\n\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-integer-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update integer timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_double_timeseries", - "name": "Update double timeseries", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAANMSURBVHja7d3fS5NRGAfw/SsrhSCKKIugburCi6KCLrropi4izME0c1kLUxquHybmKNPAi4qMJkm9hOkswhVkpaLMfhmlbjHYu+3Vaf7a3r3fLjad5nZh6NiZ3+diO+fAe/HhOc85Z+/Fjg4R2T0ieLjlMHQRT0iF4KGGPBGdHEIWREjWudVsgKhu3QiyIkYIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQghZS8jbbwDQOT9slJc/5HGJAOmoAaAUjKWERCO9z7RI5kPGC8cBxy3A5/wUBoyy1qNirg+I9jtlAPBcvGkt7xWgRmyvgKpu9JR3NF4HjHLk9DSCBmi2eqnsFwCEK8vGRSj2j9fgM85h0Ito4VgC0m/R4GwEgHZ72xMRIOHiwIv7wJ/HN6oKfAmIdL62tsoCAMps1CvE8vuwvWIIaHgehTEGmULQgPYGv9+vCLWP/Cg1a4ClG18LvDDKKPmJPgNGzwXgdQkF0S5JAPrPXqgzDcAo402R2WYAukorzUMi7uzR6YWqmY0BJzQeUQghhBBC1h9kZllDDIj2cumBaKZaijWk6hmRIFqLybl0RDJJi75EgWitJse/Y20mKf4hDkRrWe4AJJOUPB8ZC0mWj1hOUjgyDjJ9zw1Ae5rcsUKIbK9rmVzovatv+hJvfh5ec4hirfidYl6teGp1bd1/Km9XfDhasOnksZxGAIGmA/rWtU/J2NXL7tT5WEmxz+0sUqHsNsR69lwXcDc3AC13b0U6IFCsZanzsZLld1D/AYApP/5wDYBBfTfwPupPCwSKNbnj/zbE44WLOvYcLwCkCYLpVTyiODb2zDe/P7qypQnphKzioXF0myUxMw9t3+MUFBLcd2LxP1qo1bnDQkKmjhydWjqw4YGIkMiJ/MT5ufggAL9eEhCilWzudLlcLgWvbYBDfyfoPpOnCAiZ1MeiGeYdKtCcp9cfHhB11YoXeewFo2+Cv9kJIYQQQghZf5DbaQ1mhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCEkMyFZc0FwdlzZPC7rwtlxibaqy45rzVX8BUWnY6Q1/jMIAAAAAElFTkSuQmCC", - "description": "Simple form to input new double value for pre-defined timeseries key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n dataKeyOptional: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-double-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update double timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_boolean_timeseries", - "name": "Update boolean timeseries", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAIQSURBVHja7d3PSxRhHMfx+VcWgmg3ltKD7BJshyBIQusQ3vIYCILCRMyhH6xShlGWMGq1p1gIibxFULRR1F4ixcNqdNhmUXFipWnTYvfZ/TrjJqFQ13ge3p/L8MztBQ/P8/0eZr6WNHyvrHk8vy5WoxIo0TwqqDQsPxADEviWp0yAKM8qixEpAwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC5H9DSiP2noyUNIXsc9j2qKYQ2/732lzI7LyukN4jUV7uLrtndIWcuP4lzKYBELf9rD66/bzZhqw9vFcM37ReTzz5pR3kc8fAWPriDmT58NDl5F0R51j2zOm6PpB+13VfyNNbIm8O/Iwg430iz87Lu/iaNDKz+kB6Hcd5HH60PDedjfkR5FXi2oeWyI3j+Xy+56puW2vh6KXcnTZEFq90pQripAbC5HSDDDoin9qQjxVRkwk1dUrLU2v4bLAxGFuNIEPnanK/s7WSeNDcct7rBvFOxg7djBUjSLX/YDJdEHmbiccv1PQrUb7++T/B9/Xfd8sPikbK+L81VqP7HEu0ukCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAADEUYsyAYDNGNn/zrboZQ7SVZcZYcyXbTFGWg8aBYQQAAAAASUVORK5CYII=", - "description": "Simple form to input new boolean value for pre-defined timeseries key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{currentValue}}\n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\r\n overflow: hidden;\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n display: flex;\r\n}\r\n.grid__element:first-child {\r\n flex: 1;\r\n}\r\n\r\n.grid__element {\r\n display: flex;\r\n}\r\n\r\n.attribute-update-form .mat-button.mat-icon-button {\r\n width: 32px;\r\n min-width: 32px;\r\n height: 32px;\r\n min-height: 32px;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .mat-icon-button mat-icon {\r\n width: 20px;\r\n min-width: 20px;\r\n height: 20px;\r\n min-height: 20px;\r\n font-size: 20px;\r\n}\r\n\r\n.tb-toast {\r\n font-size: 14px!important;\r\n}", - "controllerScript": "let settings;\nlet utils;\nlet translate;\nlet http;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [{\n key: $scope.currentKey,\n value: $scope.checkboxValue\n }]\n );\n\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-boolean-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update boolean timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_string_timeseries", - "name": "Update string timeseries", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAOkSURBVHja7d3bSxRRHAfw/VdG6yklCbPsRk+GhVLYDSXMohC1tczVUkxFXDULzW4a+SA9VIZRm5haIJqBmMqW2cUoddUVR1sved2dnV/nuJaX3X0Q1pjZvj/YM4fDmYfP/uZcdvbhaMgmmnpVHibRShpb/4REKg9pot+mESfIC2JC1Jgkb4BIJk0veUX0AgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAsp6QN194+epPs1Z0vqm/Uw2QuuussMSOuYXYbe3PZJvyIeNx40T1xUTDTe+sHCK3STTfwQDGpgVU/+Vr+ox2FYyRktdEuS3UllFXVsAhtrMz9DOe5JI7htQfvIM1K3VcDYO9NZ+GtfP00Uz2uLEliDFHpqYy3qG2suaxGiDW86PVFURTD6/mxg4vQQwpRUW5ObyDZc5uVsX0+6A2s5uo9LmdtA7INIfUlo6MjFhUtY58S06TiXJa6HOsmc9aSd+pI576Lo6SuVNVEDndwErjhUs3dO85pCExrSSeqDE5K61bjSu7febvqJlzACdlbFEAAQQQQP4/yKxTRR0Q+eXKDdFsocFRMRTOqgkiP9E1rWwx6AzLLmqByE919avbajihxqVDsRCWj3rnVpYM1/lQLMRVPhw5ceNQHGTmnok7qlw7PAuJOrWOEIs+c8DNc+XxR2tdITSWd8XkPh8eHezrC2E5SXWfjzVNvxUdRn0Bf29V3sZvq2GVD82FuUYy5ukbFiBdN7MbeU+pOvvuV96xoyrT4kGJa8eaF8Sg/cHaEN9WIr/ixe9/8+7QxD0bsgITDgqPWMu2refChXIi28mApIiNzOYX7B826MEB76EtStC+SbLuSFkOiZZoaos/ewcUdYh9AodJ1gX8ovubekhO2Ms6hk8pcdMYlM+KmBPLIUWscoy1UF7Q4hjpEpopTMsq9cKYo6NCIacjV0MiY1hRELgImRQqKUBYiC5VQ4aEatqZ2MljWvGQYP6C9LAz5CirvBA+UUyYndXmSPGQ2GDjwG0fZ4hQPPh21xH2l5BP+oCoD7UrHtITIvgej3CGnIkWhAN9fELfLggh7YrNyLIQp92sWUOOq31QxG92QAABBBBAvBNy658GMgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAokyI1xwQ7B1HNo+LGqt3HKItabzjWHOJfgM5tA7UOw6xeQAAAABJRU5ErkJggg==", - "description": "Simple form to input new string value for pre-defined timeseries key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-string-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update string timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_server_image_attribute", - "name": "Update server image attribute", - "image": "data:image/png;base64,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", - "description": "Simple form to input new image for pre-defined server-side attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3.5, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n \n
\n \n
\n \n \n
\n
\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.tb-image-preview-container div,\n.tb-flow-drop label {\n font-size: 16px !important;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.displayPreview = utils.defaultValue(settings.displayPreview, true);\r\n settings.displayClearButton = utils.defaultValue(settings.displayClearButton, false);\r\n settings.displayApplyButton = utils.defaultValue(settings.displayApplyButton, true);\r\n settings.displayDiscardButton = utils.defaultValue(settings.displayDiscardButton, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, []]}\r\n );\r\n \r\n $scope.attributeUpdateFormGroup.valueChanges.subscribe( () => {\r\n self.ctx.detectChanges();\r\n if (!settings.displayApplyButton) {\r\n $scope.updateAttribute();\r\n }\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SERVER_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n if (settings.displayApplyButton) {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n }\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n var value = self.ctx.data[0].data[0][1];\r\n if (settings.displayApplyButton || !$scope.originalValue) {\r\n $scope.originalValue = value;\r\n }\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(value, {emitEvent: false});\r\n self.ctx.detectChanges();\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n $scope.attributeUpdateFormGroup.valueChanges.unsubscribe();\r\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-image-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"displayPreview\":true,\"displayClearButton\":false,\"displayApplyButton\":true,\"displayDiscardButton\":true},\"title\":\"Update server image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_server_date_attribute", - "name": "Update server date attribute", - "image": "data:image/png;base64,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", - "description": "Simple form to input new date value for pre-defined server-side attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3.5, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ labelValue }}\n \n \n \n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n\r\n $scope.datePickerType = settings.showTimeInput ? 'datetime' : 'date'; \r\n \r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\r\n $scope.labelValue = translate.instant('widgets.input-widgets.date');\r\n \r\n if (settings.showTimeInput) {\r\n $scope.labelValue += \" & \" + translate.instant('widgets.input-widgets.time');\r\n }\r\n \r\n var validators = [];\r\n \r\n if (settings.isRequired) {\r\n validators.push($scope.validators.required);\r\n }\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, validators]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.clear = function(event) {\r\n event.stopPropagation();\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(undefined);\r\n }\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds;\r\n \r\n if (!$scope.attributeUpdateFormGroup.get('currentValue').value) {\r\n currentValueInMilliseconds = undefined;\r\n } else {\r\n currentValueInMilliseconds = $scope.attributeUpdateFormGroup.get('currentValue').value.getTime();\r\n }\r\n \r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SERVER_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: currentValueInMilliseconds\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n \r\n $scope.isValidDate = function(date) {\r\n return date instanceof Date && !isNaN(date);\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.originalValue = moment(self.ctx.data[0].data[0][1]).toDate();\r\n\r\n if (!$scope.isValidDate($scope.originalValue)) {\r\n $scope.originalValue = undefined;\r\n }\r\n \r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n}\r\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-date-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_server_boolean_attribute", - "name": "Update server boolean attribute", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAIQSURBVHja7d3PSxRhHMfx+VcWgmg3ltKD7BJshyBIQusQ3vIYCILCRMyhH6xShlGWMGq1p1gIibxFULRR1F4ixcNqdNhmUXFipWnTYvfZ/TrjJqFQ13ge3p/L8MztBQ/P8/0eZr6WNHyvrHk8vy5WoxIo0TwqqDQsPxADEviWp0yAKM8qixEpAwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC5H9DSiP2noyUNIXsc9j2qKYQ2/732lzI7LyukN4jUV7uLrtndIWcuP4lzKYBELf9rD66/bzZhqw9vFcM37ReTzz5pR3kc8fAWPriDmT58NDl5F0R51j2zOm6PpB+13VfyNNbIm8O/Iwg430iz87Lu/iaNDKz+kB6Hcd5HH60PDedjfkR5FXi2oeWyI3j+Xy+56puW2vh6KXcnTZEFq90pQripAbC5HSDDDoin9qQjxVRkwk1dUrLU2v4bLAxGFuNIEPnanK/s7WSeNDcct7rBvFOxg7djBUjSLX/YDJdEHmbiccv1PQrUb7++T/B9/Xfd8sPikbK+L81VqP7HEu0ukCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAADEUYsyAYDNGNn/zrboZQ7SVZcZYcyXbTFGWg8aBYQQAAAAASUVORK5CYII=", - "description": "Simple form to input new boolean value for pre-defined server-side attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\r\n
\r\n
\r\n
\r\n
\r\n \r\n {{currentValue}}\r\n \r\n
\r\n
\r\n\r\n
\r\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\r\n
\r\n
\r\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\r\n
\r\n
\r\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\r\n
\r\n
\r\n
\r\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let settings;\nlet attributeService;\nlet utils;\nlet translate;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.checkboxValue\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-boolean-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_server_double_attribute", - "name": "Update server double attribute", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAANMSURBVHja7d3fS5NRGAfw/SsrhSCKKIugburCi6KCLrropi4izME0c1kLUxquHybmKNPAi4qMJkm9hOkswhVkpaLMfhmlbjHYu+3Vaf7a3r3fLjad5nZh6NiZ3+diO+fAe/HhOc85Z+/Fjg4R2T0ieLjlMHQRT0iF4KGGPBGdHEIWREjWudVsgKhu3QiyIkYIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQghZS8jbbwDQOT9slJc/5HGJAOmoAaAUjKWERCO9z7RI5kPGC8cBxy3A5/wUBoyy1qNirg+I9jtlAPBcvGkt7xWgRmyvgKpu9JR3NF4HjHLk9DSCBmi2eqnsFwCEK8vGRSj2j9fgM85h0Ito4VgC0m/R4GwEgHZ72xMRIOHiwIv7wJ/HN6oKfAmIdL62tsoCAMps1CvE8vuwvWIIaHgehTEGmULQgPYGv9+vCLWP/Cg1a4ClG18LvDDKKPmJPgNGzwXgdQkF0S5JAPrPXqgzDcAo402R2WYAukorzUMi7uzR6YWqmY0BJzQeUQghhBBC1h9kZllDDIj2cumBaKZaijWk6hmRIFqLybl0RDJJi75EgWitJse/Y20mKf4hDkRrWe4AJJOUPB8ZC0mWj1hOUjgyDjJ9zw1Ae5rcsUKIbK9rmVzovatv+hJvfh5ec4hirfidYl6teGp1bd1/Km9XfDhasOnksZxGAIGmA/rWtU/J2NXL7tT5WEmxz+0sUqHsNsR69lwXcDc3AC13b0U6IFCsZanzsZLld1D/AYApP/5wDYBBfTfwPupPCwSKNbnj/zbE44WLOvYcLwCkCYLpVTyiODb2zDe/P7qypQnphKzioXF0myUxMw9t3+MUFBLcd2LxP1qo1bnDQkKmjhydWjqw4YGIkMiJ/MT5ufggAL9eEhCilWzudLlcLgWvbYBDfyfoPpOnCAiZ1MeiGeYdKtCcp9cfHhB11YoXeewFo2+Cv9kJIYQQQghZf5DbaQ1mhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCEkMyFZc0FwdlzZPC7rwtlxibaqy45rzVX8BUWnY6Q1/jMIAAAAAElFTkSuQmCC", - "description": "Simple form to input new double value for pre-defined server-side attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-double-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"showLabel\":true,\"isRequired\":true},\"title\":\"Update server double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_shared_image_attribute", - "name": "Update shared image attribute", - "image": "data:image/png;base64,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", - "description": "Simple form to input new image for pre-defined shared attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3.5, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.tb-image-preview-container div,\n.tb-flow-drop label {\n font-size: 16px !important;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.displayPreview = utils.defaultValue(settings.displayPreview, true);\r\n settings.displayClearButton = utils.defaultValue(settings.displayClearButton, false);\r\n settings.displayApplyButton = utils.defaultValue(settings.displayApplyButton, true);\r\n settings.displayDiscardButton = utils.defaultValue(settings.displayDiscardButton, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, []]}\r\n );\r\n \r\n $scope.attributeUpdateFormGroup.valueChanges.subscribe( () => {\r\n self.ctx.detectChanges();\r\n if (!settings.displayApplyButton) {\r\n $scope.updateAttribute();\r\n }\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType === 'DEVICE') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SHARED_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n if (settings.displayApplyButton) {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n }\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n var value = self.ctx.data[0].data[0][1];\r\n if (settings.displayApplyButton || !$scope.originalValue) {\r\n $scope.originalValue = value;\r\n }\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(value, {emitEvent: false});\r\n self.ctx.detectChanges();\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n $scope.attributeUpdateFormGroup.valueChanges.unsubscribe();\r\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-image-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"displayPreview\":true,\"displayClearButton\":false,\"displayApplyButton\":true,\"displayDiscardButton\":true},\"title\":\"Update shared image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_shared_date_attribute", - "name": "Update shared date attribute", - "image": "data:image/png;base64,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", - "description": "Simple form to input new date for pre-defined shared attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3.5, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ labelValue }}\n \n \n \n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n\r\n $scope.datePickerType = settings.showTimeInput ? 'datetime' : 'date'; \r\n \r\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\r\n $scope.labelValue = translate.instant('widgets.input-widgets.date');\r\n \r\n \r\n if (settings.showTimeInput) {\r\n $scope.labelValue += \" & \" + translate.instant('widgets.input-widgets.time');\r\n }\r\n \r\n var validators = [];\r\n \r\n if (settings.isRequired) {\r\n validators.push($scope.validators.required);\r\n }\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, validators]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType === 'DEVICE') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.clear = function(event) {\r\n event.stopPropagation();\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(undefined);\r\n }\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds;\r\n \r\n if (!$scope.attributeUpdateFormGroup.get('currentValue').value) {\r\n currentValueInMilliseconds = undefined;\r\n } else {\r\n currentValueInMilliseconds = $scope.attributeUpdateFormGroup.get('currentValue').value.getTime();\r\n }\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SHARED_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: currentValueInMilliseconds\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n \r\n $scope.isValidDate = function(date) {\r\n return date instanceof Date && !isNaN(date);\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.originalValue = moment(self.ctx.data[0].data[0][1]).toDate();\r\n \r\n if (!$scope.isValidDate($scope.originalValue)) {\r\n $scope.originalValue = undefined;\r\n }\r\n \r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-date-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"isRequired\":true,\"showLabel\":true,\"showTimeInput\":true},\"title\":\"Update shared date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_shared_boolean_attribute", - "name": "Update shared boolean attribute", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAIQSURBVHja7d3PSxRhHMfx+VcWgmg3ltKD7BJshyBIQusQ3vIYCILCRMyhH6xShlGWMGq1p1gIibxFULRR1F4ixcNqdNhmUXFipWnTYvfZ/TrjJqFQ13ge3p/L8MztBQ/P8/0eZr6WNHyvrHk8vy5WoxIo0TwqqDQsPxADEviWp0yAKM8qixEpAwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC5H9DSiP2noyUNIXsc9j2qKYQ2/732lzI7LyukN4jUV7uLrtndIWcuP4lzKYBELf9rD66/bzZhqw9vFcM37ReTzz5pR3kc8fAWPriDmT58NDl5F0R51j2zOm6PpB+13VfyNNbIm8O/Iwg430iz87Lu/iaNDKz+kB6Hcd5HH60PDedjfkR5FXi2oeWyI3j+Xy+56puW2vh6KXcnTZEFq90pQripAbC5HSDDDoin9qQjxVRkwk1dUrLU2v4bLAxGFuNIEPnanK/s7WSeNDcct7rBvFOxg7djBUjSLX/YDJdEHmbiccv1PQrUb7++T/B9/Xfd8sPikbK+L81VqP7HEu0ukCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAADEUYsyAYDNGNn/zrboZQ7SVZcZYcyXbTFGWg8aBYQQAAAAASUVORK5CYII=", - "description": "Simple form to input new boolean value for pre-defined shared attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\r\n
\r\n
\r\n
\r\n
\r\n \r\n {{currentValue}}\r\n \r\n
\r\n
\r\n\r\n
\r\n
\r\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\r\n
\r\n
\r\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\r\n
\r\n
\r\n
\r\n
", - "templateCss": ".attribute-update-form {\r\n overflow: hidden;\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n display: flex;\r\n}\r\n.grid__element:first-child {\r\n flex: 1;\r\n}\r\n\r\n.grid__element {\r\n display: flex;\r\n}\r\n\r\n.attribute-update-form .mat-button.mat-icon-button {\r\n width: 32px;\r\n min-width: 32px;\r\n height: 32px;\r\n min-height: 32px;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .mat-icon-button mat-icon {\r\n width: 20px;\r\n min-width: 20px;\r\n height: 20px;\r\n min-height: 20px;\r\n font-size: 20px;\r\n}\r\n\r\n.tb-toast {\r\n font-size: 14px!important;\r\n}", - "controllerScript": "let settings;\nlet attributeService;\nlet utils;\nlet translate;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.checkboxValue || false\n }\n ]\n ).subscribe(\n function success() {\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-boolean-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_server_integer_attribute", - "name": "Update server integer attribute", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAANBSURBVHja7d3dS1NhHAfw/Sum1IUQBZYJIdSF4VVQIOSNXZiJg5nmWi18CXG9WJgSqd1IBUWu10PY1EBcIFYqyoTCLOZWA8/0uGmbbjs73y7mS+Z2YdjYs76/i3Oe88C5+PB73jhwnkeHiOxyCh4uOQxdxO1XIXiofndEJ/uRBuGXdS41HSCqS+dEWoSTEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYSQfwl59xkA+teqDfLWl9wOESC9twAo5QsJIdHI6EstkvoQX4UP6LsNzNo/hgGDrI2oCI0B0XG7DADuSzcttaMC9JG2t0DTMEZqezuvAwY5UhbEvB5a213J9A0Awg0mnwid/cM1zBpCmPQgWrGwARlv1GDvBABbd88TESDhc3Ov7wM/H99oKp/dgEgXWlqaGgFAWYl6hBh+H9rqp4COV1EYYpAA5vWwdXi9XkWoeeRLjVkDGofxqdwDg4zqrxjTY+b8HDwOoSDaZQnAeNXFVuMEDDIGKs1temCwpsE8JeLMHg2u95qVGHBR4xKFEEIIIeT/gyxvKYgB0d5sXhAtN0uxgtS8LBJEsxrtm2sko/TbTRSI9tzY92ddj1FavYgD0axbHYBklOLnI2Uh8fIRy0kCR8pBgvdcALRn8R3bhMjdrdYlAFh62mpd/Uk81J8ciGKp/56gXW27aQ3uPVqac9AJTOUePpOfMw1gom5/VpJSsnC1zpU4H9vp7KEDlSqUQ3qg6GQI4cKzwIvMU6eTBYFiMSXOx3aG38mM9wCMBUDDAIAr+YDzB9qTBoFiie/4uwmxuGJ1+CgqAYBkQhDcwSVKX+YIANi6SvKmkw7ZwUXjzL5GAEDlsT2l8wJD5o+UrO1o4S0sFhcSOH4isP7wYFdAVEikpEABgGBWF4D27KigEK06u9/hcDgUlOUNLQ7lmkRtWksZsXiEhaqsjN3mgMij1lqEPcJsY8OPD4QQQgghhOws5E5SgxkhhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCElNSNocEJweRzb7ZF04PQ7RVnXpcay5il+KVmW7YpZ2rQAAAABJRU5ErkJggg==", - "description": "Simple form to input new integer value for pre-defined server attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)\n ];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n \n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-integer-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_server_string_attribute", - "name": "Update server string attribute", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAOkSURBVHja7d3bSxRRHAfw/VdG6yklCbPsRk+GhVLYDSXMohC1tczVUkxFXDULzW4a+SA9VIZRm5haIJqBmMqW2cUoddUVR1sved2dnV/nuJaX3X0Q1pjZvj/YM4fDmYfP/uZcdvbhaMgmmnpVHibRShpb/4REKg9pot+mESfIC2JC1Jgkb4BIJk0veUX0AgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAsp6QN194+epPs1Z0vqm/Uw2QuuussMSOuYXYbe3PZJvyIeNx40T1xUTDTe+sHCK3STTfwQDGpgVU/+Vr+ox2FYyRktdEuS3UllFXVsAhtrMz9DOe5JI7htQfvIM1K3VcDYO9NZ+GtfP00Uz2uLEliDFHpqYy3qG2suaxGiDW86PVFURTD6/mxg4vQQwpRUW5ObyDZc5uVsX0+6A2s5uo9LmdtA7INIfUlo6MjFhUtY58S06TiXJa6HOsmc9aSd+pI576Lo6SuVNVEDndwErjhUs3dO85pCExrSSeqDE5K61bjSu7febvqJlzACdlbFEAAQQQQP4/yKxTRR0Q+eXKDdFsocFRMRTOqgkiP9E1rWwx6AzLLmqByE919avbajihxqVDsRCWj3rnVpYM1/lQLMRVPhw5ceNQHGTmnok7qlw7PAuJOrWOEIs+c8DNc+XxR2tdITSWd8XkPh8eHezrC2E5SXWfjzVNvxUdRn0Bf29V3sZvq2GVD82FuUYy5ukbFiBdN7MbeU+pOvvuV96xoyrT4kGJa8eaF8Sg/cHaEN9WIr/ixe9/8+7QxD0bsgITDgqPWMu2refChXIi28mApIiNzOYX7B826MEB76EtStC+SbLuSFkOiZZoaos/ewcUdYh9AodJ1gX8ovubekhO2Ms6hk8pcdMYlM+KmBPLIUWscoy1UF7Q4hjpEpopTMsq9cKYo6NCIacjV0MiY1hRELgImRQqKUBYiC5VQ4aEatqZ2MljWvGQYP6C9LAz5CirvBA+UUyYndXmSPGQ2GDjwG0fZ4hQPPh21xH2l5BP+oCoD7UrHtITIvgej3CGnIkWhAN9fELfLggh7YrNyLIQp92sWUOOq31QxG92QAABBBBAvBNy658GMgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAokyI1xwQ7B1HNo+LGqt3HKItabzjWHOJfgM5tA7UOw6xeQAAAABJRU5ErkJggg==", - "description": "Simple form to input new string value for pre-defined server-side attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n \n var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-string-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"showLabel\":true,\"isRequired\":true},\"title\":\"Update server string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_location_timeseries", - "name": "Update location timeseries", - "image": "data:image/png;base64,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", - "description": "Simple form to input new location for pre-defined timeseries keys.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? latLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n \n \n {{ settings.showLabel ? lngLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentLat: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-90),\r\n $scope.validators.max(90)]],\r\n currentLng: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-180),\r\n $scope.validators.max(180)]]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"timeseries\"){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityTimeseries(\r\n datasource.entity.id,\r\n 'scope',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-location-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update location timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_shared_double_attribute", - "name": "Update shared double attribute", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAANMSURBVHja7d3fS5NRGAfw/SsrhSCKKIugburCi6KCLrropi4izME0c1kLUxquHybmKNPAi4qMJkm9hOkswhVkpaLMfhmlbjHYu+3Vaf7a3r3fLjad5nZh6NiZ3+diO+fAe/HhOc85Z+/Fjg4R2T0ieLjlMHQRT0iF4KGGPBGdHEIWREjWudVsgKhu3QiyIkYIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQghZS8jbbwDQOT9slJc/5HGJAOmoAaAUjKWERCO9z7RI5kPGC8cBxy3A5/wUBoyy1qNirg+I9jtlAPBcvGkt7xWgRmyvgKpu9JR3NF4HjHLk9DSCBmi2eqnsFwCEK8vGRSj2j9fgM85h0Ito4VgC0m/R4GwEgHZ72xMRIOHiwIv7wJ/HN6oKfAmIdL62tsoCAMps1CvE8vuwvWIIaHgehTEGmULQgPYGv9+vCLWP/Cg1a4ClG18LvDDKKPmJPgNGzwXgdQkF0S5JAPrPXqgzDcAo402R2WYAukorzUMi7uzR6YWqmY0BJzQeUQghhBBC1h9kZllDDIj2cumBaKZaijWk6hmRIFqLybl0RDJJi75EgWitJse/Y20mKf4hDkRrWe4AJJOUPB8ZC0mWj1hOUjgyDjJ9zw1Ae5rcsUKIbK9rmVzovatv+hJvfh5ec4hirfidYl6teGp1bd1/Km9XfDhasOnksZxGAIGmA/rWtU/J2NXL7tT5WEmxz+0sUqHsNsR69lwXcDc3AC13b0U6IFCsZanzsZLld1D/AYApP/5wDYBBfTfwPupPCwSKNbnj/zbE44WLOvYcLwCkCYLpVTyiODb2zDe/P7qypQnphKzioXF0myUxMw9t3+MUFBLcd2LxP1qo1bnDQkKmjhydWjqw4YGIkMiJ/MT5ufggAL9eEhCilWzudLlcLgWvbYBDfyfoPpOnCAiZ1MeiGeYdKtCcp9cfHhB11YoXeewFo2+Cv9kJIYQQQghZf5DbaQ1mhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCEkMyFZc0FwdlzZPC7rwtlxibaqy45rzVX8BUWnY6Q1/jMIAAAAAElFTkSuQmCC", - "description": "Simple form to input new double value for pre-defined shared attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)\n ];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n \n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-double-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_shared_integer_attribute", - "name": "Update shared integer attribute", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAANBSURBVHja7d3dS1NhHAfw/Sum1IUQBZYJIdSF4VVQIOSNXZiJg5nmWi18CXG9WJgSqd1IBUWu10PY1EBcIFYqyoTCLOZWA8/0uGmbbjs73y7mS+Z2YdjYs76/i3Oe88C5+PB73jhwnkeHiOxyCh4uOQxdxO1XIXiofndEJ/uRBuGXdS41HSCqS+dEWoSTEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYSQfwl59xkA+teqDfLWl9wOESC9twAo5QsJIdHI6EstkvoQX4UP6LsNzNo/hgGDrI2oCI0B0XG7DADuSzcttaMC9JG2t0DTMEZqezuvAwY5UhbEvB5a213J9A0Awg0mnwid/cM1zBpCmPQgWrGwARlv1GDvBABbd88TESDhc3Ov7wM/H99oKp/dgEgXWlqaGgFAWYl6hBh+H9rqp4COV1EYYpAA5vWwdXi9XkWoeeRLjVkDGofxqdwDg4zqrxjTY+b8HDwOoSDaZQnAeNXFVuMEDDIGKs1temCwpsE8JeLMHg2u95qVGHBR4xKFEEIIIeT/gyxvKYgB0d5sXhAtN0uxgtS8LBJEsxrtm2sko/TbTRSI9tzY92ddj1FavYgD0axbHYBklOLnI2Uh8fIRy0kCR8pBgvdcALRn8R3bhMjdrdYlAFh62mpd/Uk81J8ciGKp/56gXW27aQ3uPVqac9AJTOUePpOfMw1gom5/VpJSsnC1zpU4H9vp7KEDlSqUQ3qg6GQI4cKzwIvMU6eTBYFiMSXOx3aG38mM9wCMBUDDAIAr+YDzB9qTBoFiie/4uwmxuGJ1+CgqAYBkQhDcwSVKX+YIANi6SvKmkw7ZwUXjzL5GAEDlsT2l8wJD5o+UrO1o4S0sFhcSOH4isP7wYFdAVEikpEABgGBWF4D27KigEK06u9/hcDgUlOUNLQ7lmkRtWksZsXiEhaqsjN3mgMij1lqEPcJsY8OPD4QQQgghhOws5E5SgxkhhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCElNSNocEJweRzb7ZF04PQ7RVnXpcay5il+KVmW7YpZ2rQAAAABJRU5ErkJggg==", - "description": "Simple form to input new integer value for pre-defined shared attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)\n ];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-integer-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_shared_string_attribute", - "name": "Update shared string attribute", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAOkSURBVHja7d3bSxRRHAfw/VdG6yklCbPsRk+GhVLYDSXMohC1tczVUkxFXDULzW4a+SA9VIZRm5haIJqBmMqW2cUoddUVR1sved2dnV/nuJaX3X0Q1pjZvj/YM4fDmYfP/uZcdvbhaMgmmnpVHibRShpb/4REKg9pot+mESfIC2JC1Jgkb4BIJk0veUX0AgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAsp6QN194+epPs1Z0vqm/Uw2QuuussMSOuYXYbe3PZJvyIeNx40T1xUTDTe+sHCK3STTfwQDGpgVU/+Vr+ox2FYyRktdEuS3UllFXVsAhtrMz9DOe5JI7htQfvIM1K3VcDYO9NZ+GtfP00Uz2uLEliDFHpqYy3qG2suaxGiDW86PVFURTD6/mxg4vQQwpRUW5ObyDZc5uVsX0+6A2s5uo9LmdtA7INIfUlo6MjFhUtY58S06TiXJa6HOsmc9aSd+pI576Lo6SuVNVEDndwErjhUs3dO85pCExrSSeqDE5K61bjSu7febvqJlzACdlbFEAAQQQQP4/yKxTRR0Q+eXKDdFsocFRMRTOqgkiP9E1rWwx6AzLLmqByE919avbajihxqVDsRCWj3rnVpYM1/lQLMRVPhw5ceNQHGTmnok7qlw7PAuJOrWOEIs+c8DNc+XxR2tdITSWd8XkPh8eHezrC2E5SXWfjzVNvxUdRn0Bf29V3sZvq2GVD82FuUYy5ukbFiBdN7MbeU+pOvvuV96xoyrT4kGJa8eaF8Sg/cHaEN9WIr/ixe9/8+7QxD0bsgITDgqPWMu2refChXIi28mApIiNzOYX7B826MEB76EtStC+SbLuSFkOiZZoaos/ewcUdYh9AodJ1gX8ovubekhO2Ms6hk8pcdMYlM+KmBPLIUWscoy1UF7Q4hjpEpopTMsq9cKYo6NCIacjV0MiY1hRELgImRQqKUBYiC5VQ4aEatqZ2MljWvGQYP6C9LAz5CirvBA+UUyYndXmSPGQ2GDjwG0fZ4hQPPh21xH2l5BP+oCoD7UrHtITIvgej3CGnIkWhAN9fELfLggh7YrNyLIQp92sWUOOq31QxG92QAABBBBAvBNy658GMgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAokyI1xwQ7B1HNo+LGqt3HKItabzjWHOJfgM5tA7UOw6xeQAAAABJRU5ErkJggg==", - "description": "Simple form to input new string value for pre-defined shared attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-string-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_server_location_attribute", - "name": "Update server location attribute", - "image": "data:image/png;base64,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", - "description": "Simple form to input new location for pre-defined server attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? latLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n \n \n {{ settings.showLabel ? lngLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n settings.isLatRequired = utils.defaultValue(settings.isLatRequired, true);\r\n settings.isLngRequired = utils.defaultValue(settings.isLngRequired, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n \r\n var validatorsLat = [$scope.validators.min(-90), $scope.validators.max(90)];\r\n var validatorsLng = [$scope.validators.min(-180), $scope.validators.max(180)];\r\n \r\n if (settings.isLatRequired) {\r\n validatorsLat.push($scope.validators.required);\r\n }\r\n \r\n if (settings.isLngRequired) {\r\n validatorsLng.push($scope.validators.required);\r\n }\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group({\r\n currentLat: [undefined, validatorsLat],\r\n currentLng: [undefined, validatorsLng]\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"attribute\") {\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SERVER_SCOPE',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-location-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "markers_placement_google_maps", - "name": "Markers Placement - Google Maps", - "image": "data:image/png;base64,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", - "description": "Allows configuring location of the selected entities on the Google Map.", - "descriptor": { - "type": "latest", - "sizeX": 8.5, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"colorFunction\":\"\\n\",\"color\":\"#fe7569\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"defaultZoomLevel\":5,\"provider\":\"google-map\",\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\",\"showPolygonTooltip\":false},\"title\":\"Markers Placement - Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"8d3c0156-0a14-7a6f-0ddd-0ec16b9ffc91\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"46bf69cd-8906-234c-a879-e2e4c92f5b67\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" - } - }, - { - "alias": "update_shared_location_attribute", - "name": "Update shared location attribute", - "image": "data:image/png;base64,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", - "description": "Simple form to input new location for pre-defined shared attribute key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? latLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n \n \n {{ settings.showLabel ? lngLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n settings.isLatRequired = utils.defaultValue(settings.isLatRequired, true);\r\n settings.isLngRequired = utils.defaultValue(settings.isLngRequired, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n\r\n var validatorsLat = [$scope.validators.min(-90), $scope.validators.max(90)];\r\n var validatorsLng = [$scope.validators.min(-180), $scope.validators.max(180)];\r\n \r\n if (settings.isLatRequired) {\r\n validatorsLat.push($scope.validators.required);\r\n }\r\n \r\n if (settings.isLngRequired) {\r\n validatorsLng.push($scope.validators.required);\r\n }\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group({\r\n currentLat: [undefined, validatorsLat],\r\n currentLng: [undefined, validatorsLng],\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType === 'DEVICE') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"attribute\"){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SHARED_SCOPE',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-location-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "web_camera_input", - "name": "Photo camera input", - "image": "data:image/png;base64,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", - "description": "Simple form to take webcamera image or upload photo.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.photoCameraInputWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-photo-camera-input-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "update_json_attribute", - "name": "Update JSON attribute", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAATZElEQVR42u2dB1sUVxeA/Sum99h7b9FEE2OiBrvGGrtijyV27L2g2Hv3U2JXRFQQQVCsEVEDqCAqig3Lfi9742QzsyzDMrvsLuc8+/DM3J3Czn3n3HPPuffcUjab7dWrVw9ERCyS3NxcoCqlqHr79q1NRKTIAkjgBFSlhCoRT7CVB5Y8CxFrJQ+shw8fyoMQsVaASsASEbBEBCwRAUvAsrhP9Pz585ycnCceEy7OLXR9+eK6r4DlJeHRP/GKcCNfuK+A5SXxqM7Q6Q9fuK+A5SV54kXxhfsKWAJWAIG1bdu28PBwAUvAci4hISHffvstkUXHwqdPn9avX3/BggUuTmzTpk3v3r2LUk+bNm0aOXKkgBWYYJ04caJ06dLHjh1zLNy1axeFiYmJHgJLjb5YtWpVnz592Hjz5o2AFWhgUanVqlULDg52LOzSpUu9evW0XfTZtWvX7ty54xQsrvDo0SNHncfuy5cvtd3Xr1/fuHEjOztb88rUqVOnZ8+egwcP5iITJkyoWrWqX8TLXdTH33///ZeDZGRk8DczM1M74Pz58+fOnbMcrKysLG7E01O71Di79+1ClRmPv3r1qnawx20sWsMvv/xS82GAxUcffbR48WK1u3nz5rJly1KCDmvWrBlPUAfW3bt3+erQoUOaNmKXszSNWL169dJ24RQeBIVJSUnz58//4osv3n///REjRnCuI4j+CNb+/ftXrFgxadKkuXPnsnHhwoU///zzypUr2gH79u2jHbAcrIsXL44aNerw4cNq9+TJk+zGxcWB1549e4zH79y5MyUlxUtgoU6odR6N2t24cSP1nZ6eznZCQsJ77723du1a1BIANWrUqFu3bubBSk5O/uSTT6ZPnw61/J66dev27dtXHcbtqlSp0qBBg9WrVweMjbV8+fJTp05pdczPZyMqKmr79u3r168HrNjYWFQXhRx2+fJlS8CaPHny7Nmz1e6SJUvYBSyeNq/048ePwSsmJmbr1q2KcmweVIP3eoU//PCDMncUMR06dFDbNH88F80GmjVrVuXKlc2DNXbsWDoBWqCAQjQfLSN6q0yZMrB19uzZzz77jNcr8MBi+8yZM9TxjBkzeD9pAQArOjp66dKl1DfVb15zuAZrgV1o+FJTU6mgZcuWcdP4+Hgg40YoMKqG/ruCD4XKKd4Da82aNVQwncF79+598MEHu3fv1r66desWZhCo0Q6iY8qVK2cerJ9++gn7qfc7CQoK4qu0tDS+ogLU8UePHgW1QAULmGgTtaYQ+2b8+PEYW3PmzLGqKYQYlBNKkRvRJi5atEgHFqYeWopKLAawOPLjjz+Gp7CwMOytZ8+eqfLr169/9dVXWNmoUOyG33//vVBgNW/eHBwX/1f8d1yrG2BR2Zg17KIzlI1Fm4htcODAAavAQiNiqk+dOpXL0sKgvXwILIRuGvbTjz/+OGzYMK2QFwuVo+3OmzfPCBZ3gSTNWeoI1qBBg77//vuS4yA1goW+p9XbsGHDtGnTFFioKyr79u3bFoLFBpbxypUr2fA5sA4ePPjhhx9iqmPraYUomE8//ZT2m22sv5o1a6LPjH4saEOrYUthjXGKBhbNHNv0ktRhaDV1WKCCBS44GrRt5W5Ao/Po+IsNwC4Pk7q30N1A34sNbBhuwcbNmzeVuwGm2cV+xdEDCaongWHHKV4FC0dU+fLlQcex4vmf8MtjdVWsWPHrr7/u3r075OGP0IGFD10dg2OC5o9tzd0AZxjsmPy1a9eGUbonJTmkQ5cQlwRGRcmKFYIz75muEMsaHRYREcHVUK08FGx848G8HygkvuV43FSOhhQvLuoQ7eXvQfGiVxvvJLpEgtAiEtIRsAQsAUvAErBE/hUZmixgeURkMoWA5RGR6V8CloinRMASEbBEBCwRAUvAEhGwRAIPLMa60NvkYMkKXDKFqsfjYHIcbyGmf7mNlLy+ASD4rhibqcZsmZngaRYsdJXbpEutBBJeDOmxMqRTlBZQ6iOQhKmdJoExBVZR2mapjABTWmbqVMASKbT4KFh0K44fP85odzNNtYiAlSfMMCEhlmuwmOHEfC8BS8AyC9bp06eZHkieBddgde7cmckUUj0ClimwmGPDXFbmQxYIVseOHZlAJ9UjYBWiKUQVFQhWq1atmAQm1SNgWQbWixcvmOLctWtXWcVOwLISLPqD5Cn45ptvtGwiIgKWZU0hqY78JdOViE+A1b9//1q1an3++eckJo2MjBTjXcDyqoO0R48eNIhSPQKWxWChzEg1QxY/UupIJQlYEisUCTiw6EWSKIwNkvsEkp4jI7J6+CTX84tM48UDFgkzyX9MRl5SkloIFhm/8alycRIzt2vXjqxaNvuIVnIZFteTteruDRs2JGyqMruSONjkWaSZnDJlCqnqiHbkV5KfsDIAIwD8CSxevk6dOpGUF7bI2m0hWCx5guuLNG79+vXTkoEzRpaVMooLLEvujlePVLPka2XSOo+OlMYmXzNywJKiHJJatGjhtCQ/IeBBwkQtBb+fNYU0VawlQZtlFVjoBvz1tBSkqiZDKyXkYCWTIjkpZ9hFtZI8NVJYDx8+XHU5SQ1PwtaZM2eSeJdyZgGQZ1ANrJg4caI6xXgWQiidPLMDBw4cMmSIWr6FtwVPCgtFcU2ndyehKIsnqNPJpKp+svE6unuR/TA0NJSB5OvWrdu7d6/2ew/apcDHwok8Z8fh58YS3fHoe/Ln+itY5JHm/fOo8Y7CIN8kuUxP2kUlVsQxS31QZ6zAQ458NAHJYUkMDgRUJ/nySVrJ+8qSUgzsqVGjhkrtqjuLi/PcyRlM0mIKVVoVvG54dDmMFKkM9THeHW2tqYrGjRtzI+N1nN7Lqfzyyy80/QU+BJIr6xAxljgKi8cwRIC7+yVYPDjWmOBBe7pXqGuMtmzZwmicfXZhAx0DWBSiDLA8KAQv6ptaV8ejNsi0azxLAQEujvdC1fG7OAaTSGWG1t09P7Acr+P0Xk4F9aYWknEhqEP+GcfDjCWOwjg5sgyjzPwSLExI3kVSh3vB3aCrWhYIad++feg7Ye0QBRYvMRrUCBZN2FK76M4yAsH/TyCBpPu0ceg5GDLeHdq0PPX5geX0Xu4J3Rf+JXpILkp0wrIUYAfQuA/Ru0VcmMirYJEWvEmTJkeOHPGOH4us4Fg5WgYwXkSqVuVpVqLAUtsaWDRGvLWcy/Ol1o1nGYFgvaTWrVur7ZYtWyqwdHenNaRlVAkU8wPL6b2cCmYQija/bxkY0rRpU8fQhbHEeB2q+LZdMOawtLRKIbaLxVnYtWS8CtalS5fIz17vnbCalKcdpNhP3Ig1MrCC2V24cCHZ57FO4JuKdwoWyxrQweQs7Hr1le4sIxCqy8aKLLzuJLhXTaHx7jhZiJOCYIUKFdSSesYm1Xgvp0KQnrrP71saNZ5zg3fCGjPGEhfX0TWFmJ54OlhbQDzv+gYRA8ixA0+fVK3LahTVFKIzHE8p8CxNJxmP0d1dWeiu334z93pmF0u8yq6vo2a18G74tMby/ZCOo40losIYuL74W3JDOiI+JQEbK6QpYbUZ9y6V36horHLNsnZj5LSfRv38ACyqnBX3cEXi6cYY9Gis0O1GDd833TfVa9OJ1pszHmMmMuhG1A9//WYHUf1NAUsvPCaWumPVrqFDh+I79mis0G2wsK/xITlNU66BZTymwMig21E/za3F9VkLU8ByJbjp6HJbCJYxVggBePlYiXPMmDGE69Xbr4vWGWOFOERUdM8xnIKjksMInFO1XNZ4jDEyiMdBc2Tgt8PRVcSoH7+LmUvSFLoS4iQ8I9Yb9qjxDgE4wXHJ7NixA88QPiFjUMUYK2RICY4lBq+CgqYzqlevTt2zRrJq/ozHGCODuIJZTlZ10YmTEGnI7/80E/XDnvvuu+/cNhlLClhEOvHLMSXawtENrh0HjRo1Qos4BUsXK1TfMpVDg4ZvR48erWsKdcc4bQpp74CMLgVcunACmYn60W4S6hHj/YGZ2A4RBvy/3gELnqDKGK0zxgqN0NB+MZDGDbDQlDTErNkZHBxcxN/CaBxtmVkBy3mvUG2w5DCxDlZb9SZYxmidMaSjtllHmA6m2qYxpRlS7mlHsByPsRkigzb72Ca6gb169XJtp7uO+ikhbO93CQe8ChavOIGqoKAgQmbYNJ62sXRg2QzRuvzAwqJC/RDHUDE1rHsGaWEJsRC6ivEZj7EZIoMIq89j3rkOhriO+tnsKwyWKVPGTK7YEt0UoqtwMvG3uDzvZqJ1yt1Ke63t0t80xu90x9gMkUEG8S1ZsqTAGwVkMgEJ6XhE6PmiLwcMGBDwHnYBS6Rkg8WAcXpADNUo7DgNEQErz4oPDw93ARZu9JCQEKkeAasQYOFBpq/kepYOIRq6S1I9ApZZsOhbEcSgH+4aLGZTue6EiwhY/xG82GFhYcRlXYOFP/Dnn3+W6hGwTIHFuADcjLiRCgTLZk+RhaVFSlKpJAGrAGH0NFNVaeMYGlCpUiVCv/mBxcgqIseEU6RvKGAVDBZhlgt2od+HpeU4iFR3Kaart23bVqpHwCqcu6HAplCMdwHLHbAK9Lxbkj5ARMD695/AH8HcdsYImBwVLiJgSaxQwBKwRPwLLFI0rXwnDPcWsAQsa8BilCZuTwFLwLIYLOYFMHlBmkIBy2KwmPWl1uSl9ydgCViWgcVUO4btMncFh4IjW1ITApY1vULy3zFMVMASsKwBS00tZJYO0ejo6GgBS8CyACzmIjPzjvAzk/vGjRsnNpaAZeW8QkY3ODoaBCwBy0obS8ASsAQskcIJeZesBItjBCwRmz0jgUlgTIFF+goBSwR1RWIf5odaBhbj091WWlIfgYEUugqqwMBMehyzYNnsyYNBtShtoohfC1UPACaTLhUCLBER8yJgiQhYIgKWiIAlYIkIWCIClkiBcud+9uCZm6t3nFym1Tjf//B/Dpy+KSXtvoDl61TV7hLiF0g5fmp1nsp/LmD5rqCr/I4q9Rkya4uA5bviLy2gszZxioDlu+KnVKmPgCVgCVgCloAlImAJWAKWgCVgCVgiApaAJWAJWMUtJApg9XIvrHpalHqt1mFyhaDxApb3hEkf5CNhHSitJDMzkxLmZ5u8AgsBk1HcC4sbuFGd5Vr/sWDTkaxHOfZf+ibi7NUmvWcLWF4CCyxYhEwrIaESJWlpaQEA1tz1hzhxb2Ti4BmbZ609mPPsZUpqZvniUF0C1n/ASk9PZ7YJU01o7FJTUx3poQVkcQ3+6sBiwhPlrEPOSi3FDlb85VvPX+SWbf3P7qItx7JznrcfuYztSm0njJy3fenWiInL9tbsPJWSoOFLJ4Tuqdt1mjq4f8iGsYt2qe1m/ebNWXdo5poDLYMXC1gWgEWbSHY4VqLnL4VxcXHqGFZhYTcyMpKvOEYDKzk5me2oqCi+YkX7jIyM4gUr/ETe75q+an+lthN1I6Wu3br74uUryHv6/OW9rMf1uk3vPn41B08JC+cADLLHOc8j466xPWDaxpe5r2lPMx48fvX6zbA5WwUsa8BSi4QnJSVRzjYLzR89ejQhIYE5mZzOwhkKLBQbGxymLks5eFnYRLpRnY16zrx2615etT1+umLnCc3A6vbHqujzyb0mrmW7z5T1HIDewiCDsJMJ1ylUkI2av6NimwkPsp+e/ysVNGlD4y7d5BhNBQpY7oN17tw5VU4DRzmpA9QBmjbSmkLWVWSDplCV03Sya+Gzcq8NApf+0zaeTkzmNXjz5i3NmSqv2n7SiHnbF24+uuNInhpesjWCwrV7T6Oc0Gf2jVcMd+k0OoxvD8dcopXkExF7hd2GPWYKWAUIbgIdWGTtcg0WX7GhpQLQwGIBMzY000pdx8LWsIjmc4tBCy/dSAevpn3nNv5tNoonPePRxn0xjmC1HRHKNpb+zfT7B09f1PRZWsbDS8np2qf5gPkCVsHCQj0kqtR2FUDqNzoFKysriw0UkipnRU8Flo4kZW9xfHGBVaPTFDCKOveXVjJvw2GuQzOHD4KNlsGLFHAaWHxu38mKu3xL4cVu84EL2F65O0q7iHudypIIFsvWQQB/6eLRm1N2lXJ4OgWLr6LsAkNoL+x3BRZC4alTp7gObjDOjY2NLV7j/cCpJNgCi98mraXhu5P56MnTFwycn7F6PxcM3X68x4Q1tJJsL98RqU4J3Xac3ZxnLyq3y7P3MafOJKVg5k9eHv7ruJWb9p+JvZgiNpbZ1vD69euKD2WVY56rr5yCpXwKMMQuCCYmJmq9wuzs7JiYmCN2iY+Pt3atYTfAqtJ+0vrwaGD65xVKudNlzApVfiL+mv23v6Up5K9q+DQV9b+IBO0i9bvPOBJzGf8q5XQMg2dtEY1VOKHHV6jIjOotGuWVXXwnpIPvgO4h1rpx/kwVQ2F+H7qH2Ox0BcSPFWgiQWgRAUvAErAELAFLwBIRsAQsAUvAErAELBEBS8DyB/HfpCBELQUs3xWSmPkpWENnbxWwfFdIjVfLPobYvz61fw25K4nXfFxIjUcSM8bf+UtaLHSVGaoELBFPiYAlImCJCFgiApaAJSJgifgRWLIIqojlAlR5YDH3Q56FiFWiVrovlZubK2yJWEsV05xK2ewTUdS6v9kiIkUQRZGaPPd/pUO/DVg1Lm4AAAAASUVORK5CYII=", - "description": "Simple form to input new JSON value for pre-defined attribute/timeseries key.", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "resources": [], - "templateHtml": "\n", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.jsonInputWidget.onDataUpdated();\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "{}", - "settingsDirective": "tb-update-json-attribute-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" - } - } + "widgetTypeFqns": [ + "input_widgets.update_multiple_attributes", + "input_widgets.markers_placement_openstreetmap", + "input_widgets.markers_placement_image_map", + "input_widgets.markers_placement_google_maps", + "input_widgets.device_claiming_widget", + "input_widgets.web_camera_input", + "input_widgets.update_server_image_attribute", + "input_widgets.update_shared_image_attribute", + "input_widgets.update_server_location_attribute", + "input_widgets.update_shared_location_attribute", + "input_widgets.update_location_timeseries", + "input_widgets.update_integer_timeseries", + "input_widgets.update_double_timeseries", + "input_widgets.update_boolean_timeseries", + "input_widgets.update_string_timeseries", + "input_widgets.update_server_date_attribute", + "input_widgets.update_server_boolean_attribute", + "input_widgets.update_server_double_attribute", + "input_widgets.update_shared_date_attribute", + "input_widgets.update_shared_boolean_attribute", + "input_widgets.update_server_integer_attribute", + "input_widgets.update_server_string_attribute", + "input_widgets.update_shared_double_attribute", + "input_widgets.update_shared_integer_attribute", + "input_widgets.update_shared_string_attribute", + "input_widgets.update_json_attribute" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/maps.json b/application/src/main/data/json/system/widget_bundles/maps.json index 21d3b97888..329ae06821 100644 --- a/application/src/main/data/json/system/widget_bundles/maps.json +++ b/application/src/main/data/json/system/widget_bundles/maps.json @@ -2,180 +2,23 @@ "widgetsBundle": { "alias": "maps_v2", "title": "Maps", - "image": "data:image/png;base64,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", - "description": "Visualize latest location or trip animation of the devices or other entities on the indoor or outdoor maps." + "image": "data:image/png;base64,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", + "description": "Visualize latest location or trip animation of the devices or other entities on the indoor or outdoor maps.", + "externalId": null, + "name": "Maps" }, - "widgetTypes": [ - { - "alias": "route_map_tencent_maps", - "name": "Route Map - Tencent Maps", - "image": "data:image/png;base64,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", - "description": "Trip animation on the Tencent maps. Allows to visualize location change over time. Use Trip Animation widget for advanced features.", - "descriptor": { - "type": "timeseries", - "sizeX": 8.5, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-route-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"tencent-map\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details
\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d3\",\"useColorFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" - } - }, - { - "alias": "test", - "name": "Trip Animation", - "image": "data:image/png;base64,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", - "description": "Trip animation on the OpenStreetMap or other map providers. Allows to visualize location and other timeseries data for each point in time.", - "descriptor": { - "type": "timeseries", - "sizeX": 10, - "sizeY": 6.5, - "resources": [], - "templateHtml": "", - "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n var $scope = self.ctx.$scope;\n $scope.self = self;\n}\n\nself.actionSources = function() {\n return {\n 'tooltipAction': {\n name: 'widget-action.tooltip-tag-action',\n multiple: false\n }\n }\n};\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-trip-animation-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 0 : (value + 2) % gpsData.length)];\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 1 : (value + 2) % gpsData.length)];\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"mapProvider\":\"OpenStreetMap.Mapnik\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"showTooltip\":true,\"tooltipColor\":\"#fff\",\"tooltipFontColor\":\"#000\",\"tooltipOpacity\":1,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"strokeWeight\":2,\"strokeOpacity\":1,\"pointSize\":10,\"markerImageSize\":34,\"rotationAngle\":180,\"provider\":\"openstreet-map\",\"normalizationStep\":1000,\"decoratorSymbol\":\"arrowHead\",\"decoratorSymbolSize\":10,\"decoratorCustomColor\":\"#000\",\"decoratorOffset\":\"20px\",\"endDecoratorOffset\":\"20px\",\"decoratorRepeat\":\"20px\",\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"pointTooltipOnRightPanel\":true,\"autocloseTooltip\":true,\"useCustomProvider\":false,\"useLabelFunction\":false,\"useTooltipFunction\":false,\"useMarkerImageFunction\":false,\"useColorFunction\":false,\"usePolylineDecorator\":false,\"useDecoratorCustomColor\":false,\"showPoints\":false,\"showPolygon\":false},\"title\":\"Trip Animation\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":false,\"showLegend\":false,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false},\"displayTimewindow\":true}" - } - }, - { - "alias": "route_map", - "name": "Route Map", - "image": "data:image/png;base64,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", - "description": "Trip animation on the Google maps. Allows to visualize location change over time. Use Trip Animation widget for advanced features.", - "descriptor": { - "type": "timeseries", - "sizeX": 8.5, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-route-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"google-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d2\",\"useColorFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" - } - }, - { - "alias": "route_map_openstreetmap", - "name": "Route Map - OpenStreetMap", - "image": "data:image/png;base64,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", - "description": "Trip animation on the OpenStreetMap. Allows to visualize location change over time. Use Trip Animation widget for advanced features.", - "descriptor": { - "type": "timeseries", - "sizeX": 8.5, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-route-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"openstreet-map\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d3\",\"useColorFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" - } - }, - { - "alias": "tencent_maps", - "name": "Tencent Maps", - "image": "data:image/png;base64,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", - "description": "Show latest values and location of the entities on Tencent maps.", - "descriptor": { - "type": "latest", - "sizeX": 9, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.24727730589425012,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.8437014651129422,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7558240907832925,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second Point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.19266205227372524,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7995830793603149,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.04902495467943502,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.44120841439482095,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"tencent-map\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details
\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.5,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "here_map", - "name": "HERE Map", - "image": "data:image/png;base64,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", - "description": "Show latest values and location of the entities on HERE maps.", - "descriptor": { - "type": "latest", - "sizeX": 9.5, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('here', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"here\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"HERE.normalDay\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"useV3\":true,\"apiKey\":\"kVXykxAfZ6LS4EbCTO02soFVfjA7HoBzNVVH9u7nzoE\"},\"mapImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.5,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"HERE Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" - } - }, - { - "alias": "image_map", - "name": "Image Map", - "image": "data:image/png;base64,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", - "description": "Show latest values and location of the entities on image map. Uses configurable background image and coordintates in range from 0 to 1.", - "descriptor": { - "type": "latest", - "sizeX": 8.5, - "sizeY": 6.5, - "resources": [], - "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"image-map\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" - } - }, - { - "alias": "google_maps", - "name": "Google Maps", - "image": "data:image/png;base64,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", - "description": "Show latest values and location of the entities on Google Maps.", - "descriptor": { - "type": "latest", - "sizeX": 8.5, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"google-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7568\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" - } - }, - { - "alias": "openstreetmap", - "name": "OpenStreetMap", - "image": "data:image/png;base64,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", - "description": "Show latest values and location of the entities on OpenStreetMap.", - "descriptor": { - "type": "latest", - "sizeX": 8.5, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"openstreet-map\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" - } - } + "widgetTypeFqns": [ + "maps_v2.openstreetmap", + "maps_v2.google_maps", + "maps_v2.image_map", + "maps_v2.here_map", + "maps_v2.tencent_maps", + "maps_v2.test", + "maps_v2.route_map_openstreetmap", + "maps_v2.route_map", + "maps_v2.route_map_tencent_maps", + "input_widgets.markers_placement_openstreetmap", + "input_widgets.markers_placement_google_maps", + "input_widgets.markers_placement_image_map" ] -} +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/navigation_widgets.json b/application/src/main/data/json/system/widget_bundles/navigation_widgets.json index 9d06c0122a..7994730172 100644 --- a/application/src/main/data/json/system/widget_bundles/navigation_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/navigation_widgets.json @@ -3,46 +3,12 @@ "alias": "navigation_widgets", "title": "Navigation widgets", "image": "data:image/png;base64,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", - "description": "Useful to define home dashboard of the user. Contains widgets that enable navigation to other dashboards and menu items." + "description": "Useful to define home dashboard of the user. Contains widgets that enable navigation to other dashboards and menu items.", + "externalId": null, + "name": "Navigation widgets" }, - "widgetTypes": [ - { - "alias": "navigation_cards", - "name": "Navigation cards", - "image": "data:image/png;base64,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", - "description": "Allows to open one or more specific pages using filter defined in the advanced settings of the widget.", - "descriptor": { - "type": "static", - "sizeX": 7, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": "/*#widget-container {\n overflow-y: auto;\n box-sizing: content-box !important;\n cursor: auto;\n}*/\n\n#widget-container #container {\n overflow-y: auto;\n box-sizing: content-box;\n cursor: auto;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onResize = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-navigation-cards-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"filterType\":\"all\"},\"title\":\"Navigation cards\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" - } - }, - { - "alias": "navigation_card", - "name": "Navigation card", - "image": "data:image/png;base64,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", - "description": "Allows to open specific page using relative path defined in the advanced settings of the widget.", - "descriptor": { - "type": "static", - "sizeX": 2.5, - "sizeY": 2, - "resources": [], - "templateHtml": "", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n\n}\n\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-navigation-card-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(255,255,255,0.87)\",\"padding\":\"8px\",\"settings\":{\"name\":\"{i18n:device.devices}\",\"icon\":\"devices_other\",\"path\":\"/devices\"},\"title\":\"Navigation card\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" - } - } + "widgetTypeFqns": [ + "navigation_widgets.navigation_cards", + "navigation_widgets.navigation_card" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/tables.json b/application/src/main/data/json/system/widget_bundles/tables.json new file mode 100644 index 0000000000..65f0775d2a --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/tables.json @@ -0,0 +1,16 @@ +{ + "widgetsBundle": { + "alias": "tables", + "title": "Tables", + "image": "data:image/png;base64,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", + "description": "Contains entity, timeseries or alarm tables.", + "externalId": null, + "name": "Tables" + }, + "widgetTypeFqns": [ + "cards.entities_table", + "alarm_widgets.alarms_table", + "cards.timeseries_table", + "control_widgets.persistent_table" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/alarms_table.json b/application/src/main/data/json/system/widget_types/alarms_table.json new file mode 100644 index 0000000000..3d3213935f --- /dev/null +++ b/application/src/main/data/json/system/widget_types/alarms_table.json @@ -0,0 +1,24 @@ +{ + "fqn": "alarm_widgets.alarms_table", + "name": "Alarms table", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAUUElEQVR42u2dh1sUVxeH/ctsUWMSSUETY/xSTDRKNNUYG4IgmhgVC7ZYA7FEERVUNAqoCIJdaeIConSwUAQEBO73zp5l3KAsEHcV3PN78vjcnXJ35s47554ZNr8zxBjz5MmTmpqakpKSOyrVCwiEAKmtrQ2ohkBVaWlpfX19e3u7UaleQCAESOAEVENAjA86KCpvCZyAagjhS2OVyrtxC6iGMDXqWKi8K6BSsFQKlkrBUilYCpZKwVIpWCoFS6V66WDxvisxMfGv3pSRkaEDquoHWJcuXZozZ87q1avX9qzly5ezTXV19aA47fT09JCQkObmZiXgVYJ15swZoPF8GfLz89nm9u3bg+K0+3JGqtcHrFWrVq34t3z0R8zBAhZ/zd2+ffuCBQsYGa+kwrt27Tp48CCNx48f+/T0BxZYe/bs4cy/+uqrKVOm7HLKz8H65ptvvvvuu3/++ee333575513Hjx48IIdXrx48fr163IPL1q0yL+mwiVLloSGhtox7Ny5c5LnMbi1tbXkRvv37//555+3bdsm2DHcZHgsYXlHR8drAxZn98YbbyQlJdHmvBiKW7du0T59+jQnGxYWVlxcfP/+fQaEf1nO6cfFxdE4f/48Z8dy2T4hIWHHjh1sf+XKFbbhI31+/PHH48eP52NMTMyhQ4fYjN7YpampyV/Amj17Ng3GheWVlZVDhw5lIUPz7rvv7t69m1XTpk1jLfn4hx9+yNPrc/ssKyvbu3evHf+6nVFFRQVr+W3aQGNrzZo1Y8eOXbduHT+ds0POm2++yelv3ryZ8+WYP/nkE1jp7OwMDAxkeW5u7ujRoxkHiAkICOAcV65cOWrUKLYHHQaWBKO8vHzu3LmzZs0qKiqKjY2dMGECu+/cuZOR9JeIdePGjZEjR9bV1b399tvciAIWlLBq06ZNP/744927d1kSHR3N4H7//fcQ9tw+2feXX37hxhW23M8IqhYvXsyXPnz4cKCBxfWW8DN8+HD4IG4tW7Zs6tSpnOyBAwc4cQacJGzevHkEpzFjxnBGUPjpp5+yQXx8PHsxgOxoz3oClvtUyFmzGbtD1d9//+0vYDGy3E+///47Nx9MuIPFHTZz5syCggKWcGdvduro0aM9dctrNthiAuUut89IqALHqqqqgUYVl5xZT+4Eh8MxbNiwq1evMjJffPHF5i5x/Fw+kKItNxXcEMPsDYhSgGWP57NgIeYEcgnw8uLbooEOFmJ0GFPYoi1gcTwcDHcY789aWlpgjjSfu5mbmwvgoWdhi1ucKYOjLSwsHLBUIX7dO2LEiMOHD8MWw8sgkCQRqMiNIKCxsZGMSrDjcYfpj2SA9okTJxgQ7j2GiA24i54LFrfiDz/8ICnp8ePH6ZyZ8TVP3hmIX3/91f7I5QcmxtQGa8aMGYw4M8K9e/dYyKpJkyZxw3GnkmF47pxHAdgKDg7maPkXiLl+AzZ/T01N5dQ4ZfIqbjDiNyQRXUY7Zb+OIQeXfEvS/KioKGIYeRVnx/8ww0d7PGnwkUZmZiYbMAK0SdhpSwrvRy9ICTOkpTKC9lT47GuI1tbWvncIWxztgI1V3URUfvaB0fOLGPDq9VmEHiCVBk8GgOXdFLNPYHGXcxlIBo/1LCYjtuEn9N4dU7oleScVlY/uOdYLwhoeHj4oqPK1iBqEt/Xr17+C91g8lDE9zfEoyV28/j6TNzTkp+43GY/KXvkWXj0rVaihocEXP2/Rn82ofCIFS6VgqRQslYKlYKkULJWCpVKwFCyVgqVSsFQKlkqlYKkULJVfg6VSeV0asVQ6FaoULJWCpWCpFCyVgqVSsFQqBUulYKkULJVKwVIpWCoFS6V6iWBhXYKfzgGPwoopKytLB1TVD7CuXbsmZqketHDhQnxBxGVVpfLTAgIq/wILg9DNbqI+j79fn+IY6z93Fe00t3c/f+OSOFOVrGA9R5g+Xr58GaM9HEdp4Pj7kodD7IcHEFipH5hTQ01Dl7FqbY71MW3i8zeuPGXuZypYPYpYJQbuorNnz+KliZO2WKURxniSoJgAPm84whHkWEsWyKqbN2/imrxlyxbW5uXlue754mIwjYyMFB9AHGDxQGcJH3EfxdOb3cUWli/Fk5JkkQHBxBCyjdOBcsOGDcZpa8iO+H9iBMeR4NkcERHByPgcrHPjTf5a18ebq6yPLrA6TflRkx1hHJtMW521oCzBVPxjNZ40W4EtK8zcjcWszjkKe0z5EZP7m3lcpWC5qKLmB3BwIcHFOMsFUIcMx1vcpCmvQAEPfFrfe+89Hl3xQsZRGM9q9mIJ2OGrieMt2F24cGHy5Mk4vQIoDTZ49OjRZ599BqMcM/1gFIhVLra5rMLzc+vWrfv27TNO41cqFcg0jX86a1nCV+NhyVEFBQWJ664PwboVZc4EmM4npr3VnHnb+ihgFe8y5z40VUnm0kyTFWItyV5s8lY4n7bmmotBpiLRnP/M3FxjLWEb9s1fbVruKViWKKBAiCJ44Br6wQcfCFhcURpgRLCRzT766CMYYoltD8x8CgR4Wc+fP/+yU3hWM80BFkzINlhOQiFm1BgwY+XNEkoQ5OTk0HguWOKvD5FvvfWW9Ekw87p7Z3ewyo+b9P+Z6rMWQ5lfWmFJwCIsNeRbISo7zKR98hSslofm1DBrFao+Z1LGms4OC6zCbToVPgXr22+/ZcaRXP6PP/5wByslJWXp0qWyGSGHijruYGGQz8wFQwQV+2kA710bLCIcnYMLJv0Eqr6Dhcc69ZLsPtndt2CVxlvZ+vX55trP1tRWEusCq/SQSR1v7uw12eEm3Q2s5gorD3tcaS2py7La7S1OsHYoWE/BooCHzQHTX69gkX1LKKJ+GIdEaSdmUrHJZ3fcp22wmPjwRhfn9OnTp0uahae+vOblAIRjSqp0A4vemHCZOqUT3xZKscA6bFrum+TRJmWMaa0zJftdYF2bY3KXWw0Hk+PHT8HqbLf2KolzrtpoMr5wTYUKljtY5EmUVqNneJK3D57BokFlLGY9smxZRQkGZjqq7jCrkjzZYIEUFFI+hLVQKFX8oIclJP5wA3Z8Lzt2A8s43bxJ1Jhk2ZEnBp+DZWE0z2SFOt8pdIFVddqijczpwlST9IaVpNs51v0LVlp25l2TGmjqchUsl7j83aoBEBXscmc0xI4bg3z7kKj/Ick7UyH7Un3OfXd2sSv9deuckENv9kJo41HR7l8qX5BUPbsj2zAn+qic4lO1N5qONmej1frP1WjsWttsxTCQetJgPSReX2DyVrlWEbdaalyPhNbJNLl2H1Bg8egENCdPnjzds6i6wTZMHK/w3Yl7juV34pVE0kgrwR9E77H4CyAPWXN6EyWTXm3JPwrdUrrCT8FiymtwDJzD6evPZphomnuTUan6C5ZKpWCpFCyVgqVSKVgqBUulYKlUCpZKwVIpWCqVgqVSsFSvJVgqlRYQUOlUqFKwVCoFS6VgqRQslUrBUilYKgVLpVKwVAqWSsHqSZ7/b1WsDXQ0Vf0DC0+OZcuW9fq/2GO76HOTDNXrBBb2m4PCFEQ1yMBSn/dXLOaBnqYCp8VS94376KPBvgkJxjemGwMLLDzWsEGbOHEi/mk06PAlX0EMI33r0NdfNTSY4GAzfLgZOdKEhJgukzCX2trMiBEmO9tqJyYacdrZssX0cdyYXoYONTU1/hKxutlxG6evn9g92rLt1/i3gdH/1x3b3vTvC8DHbk5unIu9O+ZH9nI8SHNzcz0cm4fv9YnCwsz06da1578pU0xUVPdYRY2Zzk6rMWYMPmZWg6co9wOrq3NtYMcz+3z9GSzc9PAF/emnn7DOlnoCWEUGBwdDAEagbEaDVeLUjfEacQ7LWiy4Mbe1e8Ni9MsvvxSHSKwi6YGI6HA4sIfE65H+cZcEFPxOca39+uuvMVd+rrmt7FhUVITXMo6SHBUGpz70bwIRAtXZs66P+M7HxroYmjbNDBuG6aBFBsMCc3xkeUaG2bTJzJ5tbeZwmIkTzdixZvx4I1URtm61Po4bZ4U09vVnsGgLEPQMHwIWlrU0KDnG1CnutAEBARweYIGIc4pogzb4wAUezmQbaMNBDrCEQvTnn3+KUxxssaXpzTWZsgPGaRUGXpQRkMOLlYvtC/EVXPhnK2UAELcN9RBssNwjlg3WjBnGWfrAYBjOsBDhVqywYCJiMYFevOjXYM2aNQuSfnRqDGPXHztuDNyBEgt42R34qCTg7vPOx9DQUEpREPz6bsddVVU1atQo6fPzzz9fsWKFr8DiknPhncfTHay0NKvhASwyB6Jdt+IGSUncQ2bqVCtpS0nxa7AoVZeWltbQpb6DFR4ezksQCghQxsLenbhlg8XpwJOY3lIWrxtY1FPZu3ev6aGAwPvvv09mJn36cCokHwoIMIcPuz5y2CEhfQULMeXJNIpFLwVguMpMlw6Ha5Wfg8XLM4IWxv9UFuHSmj7YcZM8sTHXnpQfY2NctfnIORLDOAUbLJgghrEK695x48bRm3EWwoiPj6dUzokTJ5hVeS1HqZVn7bg50+joaPxOWSLzsq+0Z48Fwf79BspJj3j06wmswEDDTcWDoQ0W0yWpWHKyCQqysn4GDbA4d/zradCVv4FFqRJK39gfk5OTmbCYccDLOIs0iUs2KVSiDLT1iL2F0CIFBKKioghX2fIQbkxBQQGe73ZMolCA3Tl27SynUAAwXSTnwII/O5snA3YhthG0WMtX2EWabIb4LtYyh8bExJDP+fbBkPkrNNRwCwlMxrKu56xcIY2J2Fl8ygpOvJjAdJ6GpH1gJ0EOOuU12MmT1jYs3L7d6o1oze6+ebZ9rV6Q+rUd92B88858ATT8ufDXnkWQIB+SWPKqRNRhptOLOmjAkqIPf3kUWZEU+FOpjP4eS6VgqRQslYKlYKkULJWCpVKwFCyVgqVSsFQKlg6ESsFSKVgqBUul8j5YKpX6vKt0KlQpWCqVgqVSsFQKlkqlYKkULJWCpVIpWCoFS6VgPVeY+5zuTWqZrOofWHl5eXP6oPnz5+McpGOqMv1ym8GAykNliqysLLXjVv0XsF6CjVF5efmLO4sUFhbmuNkrYpVTWlra307w4sJI0n0JnrZimMst9HKy0nvN9y5XX5X23YaSwroiaWfdyyl7VN7f3tLLM2qaa/wULGz1QsQN8QWEy97GjRvtjxEREdjz9bcTXN3O2nbFXVkmRpLG6cBGbmCc3rjuBnFeFzDNSw1ubGukvTM3OvLKGhodpmNRemjug7z+9naoIOFO/V0Fy7iHHGwagUMM1uPi4qTBBcYJUlJA27/PA1jYG8uObE/UMU7fZQDCUguGODvcuWGlxmmdSNiT2InjMruzHNNKAYvlrMUfEOPusLAw7P/YUfxRibj4PXnr2nR0doSkL7710NFhOhelhcLTo9ZH5Y8qbNpy7ueevHPqWvV1tuTjhYqLBXWFSXeSWzpaUkrOENVSSs9kVGayO2szKy9UNFY0tjWxqryxnM2uVl/rdK6it9OlZ9PK0h21BXkPbvoFWKdOnZoxYwZXMTIyUhxHMYMEC6431rTiJrpkyZLMzEx3sPCGLO8Sho4CFo6jmIvSwLUWo0dZQrcAivlxUFAQ3w5kM2fOZNXRo0fXrLEiBFvyUAJ5s2fPFrDWrl175MgRfFA5XywqAZEDYHvZGG9SL973W7K2Jt89TaQhXO3Iic6qyb5UeXnV5UhWJRafWHkpEiCWXljOvyxZeWnV4vPhO3NiHrU2zk1duPbqhuPFJ1gCMaxdduH3S5VXmF5ZteXG1mO3ExekBYMRI7n26voN1zcfLToGvn/n7/MLsIgKUioC87cJEybU1tZihMzl5+uIFvjesgoDd/fCE4A1efLkkC5hbusBLFmClSiOusZZb0JMv22wAgMDpQiKPRUKWDTAV+ITE6L4y1OOAJS9CNaJ4pPRubuIMfFFR86WpR4qiD9UGH/AcZBVJEwEnuL6O2ywMydawDpTmirTJfQU1lo5GaDE3orrBlZtSy1LgIm4VdpQxpKGVqvORawjzl/Aoq6OOGYjggqOyNiswxOTI5eTbyTFxuq4LzmWB7CIfLbH5OjRo93BEs48gwWOhE9GQEoceFH5D/IJSFuzthFamNqIW+uubrxcZbm3p5WnsQrg1l/bRAYmYDHf2WDdrr0trOzL398NrLoW663QHze2MpPmP7gVnO4a8wP+AxbTkLgdi996Y6OVW1DXBFNuikokJCTQOGw7oXsEi4AnsWe7U30Ei1lS3L8xOH0WLGZq2QurZqqqUFvPu2A1PWmal7qQ7Iq0iTkrPGMp8xdwsIoJ8VxZmhOdAzty/vzPYD14/JCkTR4zo3N2vbZgcevbsxi98ZBP8RKuJTDZl42Km8yDxlmhc/jw4ZVS9ao3sCiVQ+kbEjW84PsOFi7wkyZNWrlyJfGyG1h8ETO11ErhIWDYsGG+8PZdfWUtc5a0Y/J2w5Zk3MduHyclIj2CGLb5z2DROOg4TCq25sq68MyI1xMsnq1uuUmKeMnbV2ZAezPe79vzo4QTd0FbjZspPtjJ3wO44zlISCVREwJ4oJOiYvRmM+Fw1m6Q4gOyhN7I8zgYMZpnud0hT6MSBUmtBDuvi4jy8HGttOtb62uan7LLE2JlUyXPdDSsA2uuJm23Dsx0EoFa2q1iyqRTklHJlk/a21klT5FkaXRI41Gb9bBZ3VRD5DtSeOzlgcUzEdBgn3+jZ1G4hm388+/ZZHs8uqbZBv+DTRtvbCaNI1ELy4hwB9fnYJWVlS1YsKDXvxUyy/hnyXFedvi28ImPRRjjBRgv9CXgvTywjLMyUWlv6lZsUuXP0t9jqRQslYKlUrAULJWCpVKwVAqWgqVSsFQKlsqvweJvq/KbXZXKKwInoBrCX+/r6+t1OFTeEr/+AKohbW1t/KUPtjRuqV48VgESONEYwmd+jQlihC81kVa9iEAIkCRC/R9f3OEsEgi6eAAAAABJRU5ErkJggg==", + "description": "Displays alarms based on defined time window and other filters.", + "descriptor": { + "type": "alarm", + "sizeX": 10.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-alarms-table-widget-settings", + "dataKeySettingsDirective": "tb-alarms-table-key-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-alarms-table-basic-config", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true,\"entitiesTitle\":null,\"alarmsTitle\":\"Alarms\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"warning\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false,\"configMode\":\"basic\",\"alarmFilterConfig\":null}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/asset_admin_table.json b/application/src/main/data/json/system/widget_types/asset_admin_table.json new file mode 100644 index 0000000000..79063537ff --- /dev/null +++ b/application/src/main/data/json/system/widget_types/asset_admin_table.json @@ -0,0 +1,22 @@ +{ + "fqn": "entity_admin_widgets.asset_admin_table", + "name": "Asset admin table", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Customized entity table widget with preconfigured actions to create, update and delete assets.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-entities-table-widget-settings", + "dataKeySettingsDirective": "tb-entities-table-key-settings", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Add asset

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Asset name\\n \\n \\n Asset name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n customDialog.customDialog(htmlTemplate, AddAssetDialogController).subscribe();\\n}\\n\\nfunction AddAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.addAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addAssetFormGroup.markAsPristine();\\n let asset = {\\n name: vm.addAssetFormGroup.get('assetName').value,\\n type: vm.addAssetFormGroup.get('assetType').value,\\n label: vm.addAssetFormGroup.get('assetLabel').value\\n };\\n assetService.saveAsset(asset).subscribe(\\n function (asset) {\\n saveAttributes(asset.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Edit asset

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Asset name\\n \\n \\n Asset name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n customDialog.customDialog(htmlTemplate, EditAssetDialogController).subscribe();\\n}\\n\\nfunction EditAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.asset = null;\\n vm.attributes = {};\\n \\n vm.editAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editAssetFormGroup.markAsPristine();\\n vm.asset.name = vm.editAssetFormGroup.get('assetName').value,\\n vm.asset.type = vm.editAssetFormGroup.get('assetType').value,\\n vm.asset.label = vm.editAssetFormGroup.get('assetLabel').value\\n assetService.saveAsset(vm.asset).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n assetService.getAsset(entityId.id).subscribe(\\n function (asset) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.asset = asset;\\n vm.editAssetFormGroup.patchValue(\\n {\\n assetName: vm.asset.name,\\n assetType: vm.asset.type,\\n assetLabel: vm.asset.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteAsset();\\n }\\n }\\n );\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/attributes_card.json b/application/src/main/data/json/system/widget_types/attributes_card.json new file mode 100644 index 0000000000..5f84ec3b8a --- /dev/null +++ b/application/src/main/data/json/system/widget_types/attributes_card.json @@ -0,0 +1,20 @@ +{ + "fqn": "cards.attributes_card", + "name": "Attributes card", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays one or more latest values of the entity. Supports multiple entities.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "", + "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", + "controllerScript": "self.onInit = function() {\n \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n \n for (var i=0; i < self.ctx.datasources.length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Attributes card\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/bars.json b/application/src/main/data/json/system/widget_types/bars.json new file mode 100644 index 0000000000..f2f0f1e9e4 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/bars.json @@ -0,0 +1,25 @@ +{ + "fqn": "charts.bars", + "name": "Bars", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAA8FBMVEUhlvNMr1Bqamp5eXl7e3t8fHx9fX1+fn5/f3+AgICCgoKDg4OEhISGhoaHh4eKioqMjIyNjY2Ojo6QkJCRkZGSkpKWlpaXl5ebm5udnZ2enp6goKChoaGkpKSnp6epqamsrKyurq6xsbGzs7O1tbW2tra3t7e4uLi7u7u9vb3BwcHCwsLDw8PGxsbKysrNzc3Ozs7R0dHS0tLT09PZ2dna2trc3Nzd3d3e3t7g4ODh4eHj4+Pk5OTm5ubn5+fo6Ojp6enu7u7w8PDz8/P0Qzb09PT29vb39/f5+fn6+vr7+/v8/Pz9/f3+/v7/wQf///+dc+aLAAAAAWJLR0RPbmZBSQAAAcFJREFUeNrt3ds2AgEYhuHsaSOZbAvZi0r2YYjCJOW7/7txZhkcDNbM6h/vdwfPmlX/ybtmEorJErGCeJLadz3rkKPpZamaLTp925DHdFvSpKelU9uQ/cLKhtcdk7YqtiHruevtojch7ZZtQ0o1dcdfRqXNqm1IbVU3OaVamm/YhvQW5zIXOknnC5JUt7qEpE5fUv/5HVePy2UHAgQIECBAgAABAgQIECBxgrwGHBAgQIAAAQIECBAgQIAAAQIECJC/QRIBN0iQ+66voDMLuRp2fQWdVUhvNun6CjqrkJ0Dx/UVdEYhzXzfcX0FnVFIrlSZ2mx/LOiMQuqHh6k972NBZ/fv13G/KeiCQkIu4358EL8UdBafSGwuOxAgQIAAAQIECJDYQB4CDggQIECAAAECBAgQIECAAAECBAgQIECA/BrSufn0DjqjkEZmLXkWaUEXEmThXMeFSAu68H4j5b1IC7rQILfZTqQFXViQ1nRTL1EWdCFBnmYuJUVZ0IUEWR1xHKcXZUEXFDLwBR2XHQgQIECAAAEC5H9ChgIOCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgxiBmv+L6Bl9pkxYph15gAAAAAElFTkSuQmCC", + "description": "Displays latest values of the attributes or timeseries data for multiple entities as separate bars.", + "descriptor": { + "type": "latest", + "sizeX": 7, + "sizeY": 5, + "resources": [ + { + "url": "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js" + } + ], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \n var barData = {\n labels: [],\n datasets: []\n };\n \n for (var i = 0; i < self.ctx.datasources.length; i++) {\n var datasource = self.ctx.datasources[i];\n for (var d = 0; d < datasource.dataKeys.length; d++) {\n var dataKey = datasource.dataKeys[d];\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n var dataset = {\n label: dataKey.label + units,\n data: [0],\n backgroundColor: [dataKey.color],\n borderColor: [dataKey.color],\n borderWidth: 1\n }\n barData.datasets.push(dataset);\n }\n }\n\n var ctx = $('#barChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'bar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false,\n scales: {\n yAxes: [{\n ticks: {\n beginAtZero:true\n }\n }]\n }\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var c = 0;\n for (var i = 0; i < self.ctx.chart.data.datasets.length; i++) {\n var dataset = self.ctx.chart.data.datasets[i];\n var cellData = self.ctx.data[i]; \n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n dataset.data[0] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-chart-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Bars\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/basic_gpio_control.json b/application/src/main/data/json/system/widget_types/basic_gpio_control.json new file mode 100644 index 0000000000..05b1f2645e --- /dev/null +++ b/application/src/main/data/json/system/widget_types/basic_gpio_control.json @@ -0,0 +1,21 @@ +{ + "fqn": "gpio_widgets.basic_gpio_control", + "name": "Basic GPIO Control", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAND0lEQVR42u2cCVBVZRvHqcmpGbdSZ+SCgIYhmhskiBqgCMqSGoaiDIvgAiiCmpk6NpbVpI5WblOpn7h8bohSosiOCMiOuI5rmvip+bmh4wYU31/ezzOny13OhQuN9P/NO845x3PPgfv+eLfzPMektra2urr61q1bV69evUJII4BCEKmqqgpSmcCqioqKysrKmpqaWkIaARSCSNAJUplAMezwSyHGAjpBKhM0X2yriHHbLUhlgq6R3wUxLpCKYhGKRSgWoVgUi1AsQrEIxSKEYhGKRSgWIRSLUCxCsQihWIRiEYpFCMUiFItQLEIoFqFYhGIRQrEIxSIUixCKRSgWoViEUCxCsQjFIoRiEYpFKBYhFItQLEKxCKFYhGIRikUIxSIUi1AsQigWoViEYhFCsQjFIhSLEIpFKBahWM3BEVfXQ126sCgpN5OSKBbFolgUi2JRLIpFsZpPrOSuXTMHDMh2ccl2ds6wt0+2smrSCk7p3h23y3JyyrCzw60pVgsUC3WcN3r0sZkzy2fNkkrZjBm5Xl5NUeWwtjgoSH6vY9HRBX5+aT17UqyWI1Z6796l4eHyapaX4rCwJBubX8zNNZZEc/MkgxpFC4tcb29t94LZ6YMGab1Xly4HzM3/uWJVVVU17OrPnj1rfrFSbWxKIyK01bQoBZMnb7Gw+JepqcayydR0u0qVqKzKcz09dd+rPCYm3s5O271QNqtUexunlw6xmqju/vjjj5qamgaK9eeff3711VdWVladOnWysLD46aefxPH58+e/VUfHjh3t7Ow2bNggjltaWh4/flx8cMmSJR06dHj99df79OmTnp5e/8fKyMgICgqaM2eOQWIlW1oW+fv/Jz6+wNdX27dcMG6cnpquK6ne3joqGyXW1PQXffWd3qePknuVRERs1u6xKDvNzDTeoiQ4+E5+/t3CwgvffmuQWEeOHHFwcEAdoSLwVT948AAHS0tL33qBtbV1eHj43bt3cfzjjz9etGiR+ODhw4f79u2LusM5ixcvRmXJL1tZWTlhwgQzM7POnTtPnDjxyZMnBov1448/vv322+fPn8f22bNncSGhSExMTHR0tPCjqKhIpVL9/PPP2G3Xrl1ZWRk2li9f3rt370uXLuGEffv24fi5c+fkVx4/fvzQoUPd3NwiIiIMEutYZOTlDRtu5+YWBwRo/IrTevRQUtNiDKS3smNVqoM6xVIoMUqii4vue6HUb7dSunV7dOUKBmqYdtwrK8v18FAo1vXr19u3b79161b8kT969MjPzy80NBTHCwsL4Zk45/fff//oo4/GjRuH7cjISLiFjYsXL6K+9uzZgwbp8uXL/fv3//rrr+VXnjVrVkBAQHV1NZRycnL67rvvDBbL0dFx48aN0u727dt3794tF0uAH2j69OmSWPhNzM3NU1NTpRPCwsJmz54tv7L46/nss88MFUuUa3Fx2sQ6MnSowppG2efg0IDKljefapMDHSVn4kS999qqUtUXC7+R2MafU5aDg0KxVq1aNWzYMGkXughv5GIB1Bf8k4v1ySefhISESCegYzE1NZU3Wmg1RPMhJNPW5+gSCz9BSUlJ/eNqYnl6en755ZeSWDdv3jQxMUGDKW/50DjVv05TiJX3wQfKxUoaPlxvZWvroZ4P5mxtld+rODxc771QtM0byqZOxW+tvCucOXMmar3+t6om1rZt22xtbeVieXh4/PDDD9IJDx8+RG2i/at/qcePH9vY2KDDNVisV155RXRhBw8enFaHGE5BLPR02EVTZG9vj2GW6KeFWBcuXHj11VfRbknX2bVrFzr75hHr6OjRBojl7t4YsdBDKb9XyfTpSsTS2PMe9fG5W1SESYlysQIDA/H1YuP27dvTXoBKgVgYPIndUaNGYfSMypWLhd5tx44d8ku1atVKbSQjmDx5MkxoyOAd477s7GxsnDlzJi4uDkM2DNaEWF5eXjiSkJCAwaDUTgqx7t27B8fRf8ubZbRqzSNWjru78sr+ZfBgvTUdp10srIcpv1deSIjee22u1xWiZA8Zcq+4ONPOzqBZIepo6tSp2MAACzW1bt06VApqCmK1adMmrg5U7v3798X5klg+Pj7yYRO8xAfxr9r10Ue5uro+ffq0IWL5+vqKmwkwQ5TEkneFEtLgHTPBTZs2Scfxs+KzzSNWer9+yit7m7W17prGuoPudaaSSZMU3kvvJFSbxI9+++3G/v2X1q1DKZsyRaFY8fHxmKRLSwbPq/mFWPKuUEISa+nSpcOHD5eOo/Xq2bOn2skYeaOnkqQ0WKwTJ05gwonB2unTpzMzMwcOHCi6bb1iHThwAK1dbGws5ozz5s3DUoXoK40l1mEnpzRbW23LlcXKKjvb319vTe/R3lz9f67g4qJQrB09eui+1xYtM9A8Ly+pZA0cqFAszOmcnZ3HjBlTUFBQXl6OITkm9aIr1C0WdOnWrVtUVBQ+iOkaPrV37175mYmJiahcdKAlL2jIAimUmjRpEpQaOXLk999/L/4CYIy8QZIIDg7+9ddfxTZEHDt2LDps/MQVFRUaL46x1/r1642+QJrRvz/WJHVXc2lU1HYbG92LWHv1WSUmhsUhIXqtSvPx0W3Vdn3rGg1YIEUn+MUXX7i4uAwZMgRjeVHLGAGLdQc1MHresmWL2L5x4wbOHzRo0IcffpicnKx2JjQY91fk4+kW/kjn8Pvvl0dH63jMkjRgAFojjSXe3DzBkKc6GFOXTZumw6r8gID4Ll203W5v3UMkPit8aR5CZ9rbl4SFaZj2h4TgSaKRgxqsrQv9/TU+zMFjxEOWlnwI3bLCZiwtM997L8/bG+vj+X5+GKNk6JxbNbKkv/suNIJhRcHBuF2OhwciLBg2w0A/BvpRLBaKRbEoFsWiWBSLhWJRLIpFsSgWxWKhWH+jWHich6cuqe+8c8jCgmJRLCOIleXoWBQQcOzFQ0M8Iizw92/SxXeK1cLFQqh44fjx2p4KH8VD+6Z/fkexWppYz1PEAgN1x7Ec9vNDuKbGgriobSpVvJlZEsVqOrHy8xH0EYBUiClTppw6dUocRDyWCMRBQOmCBQtEfljtX+Ox8vLy8L/u7u5z586VhykL7ty5g7Ay/C/ippElplwsRAOfWby4Yteuk3Pnahsz6c8grSv7nZ315swYJU2ZYqmTk5OD3KC1a9ciSnD16tWIPBQOIYIUIWBpaWmIAkPsMwJHEWtaK4sgTUlJwcmIs0YeIoL2kZwoT9oBiD5D2hCiCFesWNG9e3e1lEgdYl1YseLS2rUIpLyekFA+Y4bGTlBhSlZxRMQmMzM9bpmaUizji4UMiM8//1zahQorV66srReajMZMRLVKYiEgWh4aipZp2bJl0i7CUGEbkofE7htvvIHESIVi5Y4YIYKST3366blvvtEQ4jd4sPKY9z39+umNTt6nII6UYhkmFvJOc3Nz6x/XLRaCphG3j+xC6QRkfXh7e2u8Bdo2RMRrTPbQNsbKcXO7uHr1nYIChPJpME/7+znqlwOurvrTvzRlzlCsRomF9ECRUCbSvwAC7Gtl6V87d+5cuHChWleIpFu1vEKchqRqtYsjVRL5/6+99po8Z1qJWM9DQ4OC/puVVRoaqiEFb8wY5WIljxihV6x/Uyyji9W6dWuk2dS+SFhF0qmU/iUSVpEogcR+acAuxEIoPloskUQvQLcoT/eWQG+IfArkgWjM4dEoFtKtxBunkP51bffuBo/clb9PYSe7QqOLhTyNNWvWSLsK8woxEke2v/wNM3ilCbI+pF1kbeNNIdIu0tbwggCFYl1cteq32Nj8UaNuJCYej4nR0J45OioXa2evXnrFSngZJoYvmVjo7PAmGfFmIrxmKbCOWgV5hVAQefci4R95YJhanjx5Ut5QIRkXCxnYxrD9zTffvHbtmtLlBiur8qgo6FUSEqJ5EcvKqiwyUolVeEVWy+gHX8p1LLwTC80PxteofoyrxPRNr1jIlsT6Vtu2bbHo0LVr1/3796udiSM4jrYK2bp41Y5xF0gVJpHGDxigN9cviQukTbryjoleA97Nhz5RbflKDfk4zLiPdPJ9fXVble7pidZIW9mFN369JEujfKTTrM8Kn78XFKN4TTmrWD494ubGh9AUq+ElrVevPB8fpK1CJpTi0FC8MjlF+8uAKBbFYqFYFItiUSyKRbFYKFaDuLRmDUKvWJSUB2fOUCzyj4BiEYpFKBahWBSLUCxCsQjFoliEYhGKRSgWIRSLUCxCsQihWIRiEYpFCMUiFItQLEIoFqFYhGIRQrEIxSIUixCKRSgWoViEUCxCsQjFIoRiEYpFKBYhFItQLEKxCKFYhGIRikUIxSIUi1AsQigWoViEYhFCsQjFIhSLEIpF/l6xrl69WlNTw++CGAvoBKlMbt26VVlZya+DGIv79+9DKpOqqqqKigq4xXaLNL6tgkjQCRsm2K+uroZiaL6uENIIoBBEEi3U/wDz+OXgAWa1/wAAAABJRU5ErkJggg==", + "description": "Allows to change state of the GPIO for target device using RPC commands. Requires handling of the RPC commands in the device firmware. Uses 'getGpioStatus' and 'setGpioStatus' RPC calls", + "descriptor": { + "type": "rpc", + "sizeX": 4, + "sizeY": 2, + "resources": [], + "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n {{rpcErrorText}}\n \n
", + "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel mat-slide-toggle {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel.col-0 mat-slide-toggle {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 mat-slide-toggle {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", + "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-control-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n \n var i, gpio;\n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n scope.gpioList = [];\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false\n }\n );\n }\n\n scope.requestTimeout = settings.requestTimeout || 1000;\n\n scope.switchPanelBackgroundColor = settings.switchPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioStatusRequest = {\n method: \"getGpioStatus\",\n paramsBody: \"{}\"\n };\n \n if (settings.gpioStatusRequest) {\n scope.gpioStatusRequest.method = settings.gpioStatusRequest.method || scope.gpioStatusRequest.method;\n scope.gpioStatusRequest.paramsBody = settings.gpioStatusRequest.paramsBody || scope.gpioStatusRequest.paramsBody;\n }\n \n scope.gpioStatusChangeRequest = {\n method: \"setGpioStatus\",\n paramsBody: \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n };\n \n if (settings.gpioStatusChangeRequest) {\n scope.gpioStatusChangeRequest.method = settings.gpioStatusChangeRequest.method || scope.gpioStatusChangeRequest.method;\n scope.gpioStatusChangeRequest.paramsBody = settings.gpioStatusChangeRequest.paramsBody || scope.gpioStatusChangeRequest.paramsBody;\n }\n \n scope.parseGpioStatusFunction = \"return body[pin] === true;\";\n \n if (settings.parseGpioStatusFunction && settings.parseGpioStatusFunction.length > 0) {\n scope.parseGpioStatusFunction = settings.parseGpioStatusFunction;\n }\n \n scope.parseGpioStatusFunction = new Function(\"body, pin\", scope.parseGpioStatusFunction);\n \n function requestGpioStatus() {\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusRequest.method, \n scope.gpioStatusRequest.paramsBody, \n scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n for (var g = 0; g < scope.gpioList.length; g++) {\n var gpio = scope.gpioList[g];\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled; \n self.ctx.detectChanges();\n }\n }\n );\n }\n \n function changeGpioStatus(gpio) {\n var pin = gpio.pin + '';\n var enabled = !gpio.enabled;\n enabled = enabled === true ? 'true' : 'false';\n var paramsBody = scope.gpioStatusChangeRequest.paramsBody;\n var requestBody = JSON.parse(paramsBody.replace(\"\\\"{$pin}\\\"\", pin).replace(\"\\\"{$enabled}\\\"\", enabled));\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusChangeRequest.method, \n requestBody, scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled;\n self.ctx.detectChanges();\n }\n );\n }\n \n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n scope.gpioClick = function($event, gpio) {\n if (scope.rpcEnabled && !scope.executingRpcRequest) {\n changeGpioStatus(gpio);\n }\n };\n \n scope.gpioToggleChange = function($event, gpio) {\n gpio.enabled = !$event.checked;\n $event.source.toggle();\n self.ctx.detectChanges();\n }\n \n if (scope.rpcEnabled) {\n requestGpioStatus(); \n }\n \n self.onResize();\n}\n\nself.onResize = function() {\n var scope = self.ctx.$scope;\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.prefferedRowHeight = prefferedRowHeight;\n var ratio = prefferedRowHeight/32;\n \n var css = '.mat-slide-toggle .mat-slide-toggle-bar {\\n' +\n ' height: ' + 14*ratio+'px;\\n'+\n ' width: ' + 36*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb-container {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-ripple {\\n' +\n ' height: ' + 40*ratio+'px;\\n'+\n ' width: ' + 40*ratio+'px;\\n'+\n ' top: calc(50% - '+20*ratio+'px);\\n'+\n ' left: calc(50% - '+20*ratio+'px);\\n'+\n '}\\n';\n css += '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n\n cssParser.createStyleElement(namespace, css);\n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gpio-control-widget-settings", + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"Basic GPIO Control\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/basic_gpio_panel.json b/application/src/main/data/json/system/widget_types/basic_gpio_panel.json new file mode 100644 index 0000000000..1752d60385 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/basic_gpio_panel.json @@ -0,0 +1,21 @@ +{ + "fqn": "gpio_widgets.gpio_panel", + "name": "Basic GPIO Panel", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to display state of the GPIO for target device using latest attribute values. You should set the label of the selected data key to GPIO pin number (e.g. '1') and use boolean values for widget to display the data.", + "descriptor": { + "type": "latest", + "sizeX": 5, + "sizeY": 2, + "resources": [], + "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n
", + "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", + "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-panel-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.detectChanges();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var css = '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n \n cssParser.createStyleElement(namespace, css); \n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gpio-panel-widget-settings", + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"color\":\"#008000\",\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"color\":\"#ffff00\",\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"color\":\"#cf006f\",\"_uniqueKey\":2}],\"ledPanelBackgroundColor\":\"#b71c1c\"},\"title\":\"Basic GPIO Panel\",\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"1\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.22518255793320163,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"2\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7008206860666621,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"3\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.42600325102193426,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 1000;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/compass.json b/application/src/main/data/json/system/widget_types/compass.json new file mode 100644 index 0000000000..03f8d6a0aa --- /dev/null +++ b/application/src/main/data/json/system/widget_types/compass.json @@ -0,0 +1,21 @@ +{ + "fqn": "analogue_gauges.analogue_compass", + "name": "Compass", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays latest value of the attribute or timeseries key on the compass. Expects value to be in range of 0 to 360.", + "descriptor": { + "type": "latest", + "sizeX": 6, + "sizeY": 5, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueCompass(self.ctx, 'compass');\n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-analogue-compass-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"minorTicks\":22,\"needleCircleSize\":15,\"showBorder\":true,\"borderOuterWidth\":10,\"colorPlate\":\"#222\",\"colorMajorTicks\":\"#f5f5f5\",\"colorMinorTicks\":\"#ddd\",\"colorNeedle\":\"#f08080\",\"colorNeedleCircle\":\"#e8e8e8\",\"colorBorder\":\"#ccc\",\"majorTickFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ccc\"},\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"animationTarget\":\"needle\",\"majorTicks\":[\"N\",\"NE\",\"E\",\"SE\",\"S\",\"SW\",\"W\",\"NW\"]},\"title\":\"Compass\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/dashboard_state_widget.json b/application/src/main/data/json/system/widget_types/dashboard_state_widget.json new file mode 100644 index 0000000000..ab66c2bac1 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/dashboard_state_widget.json @@ -0,0 +1,21 @@ +{ + "fqn": "cards.dashboard_state_widget", + "name": "Dashboard state widget", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays specified dashboard state inside widget. Advanced widget settings allows you to configure target dashboard state to be displayed.", + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "\n\n
\n
Dashboard state widget
\n
(Specify dashboard state id in the advanced widget settings)
\n
\n", + "templateCss": ".dashboard-state-widget-prompt {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n font-size: 32px;\n font-weight: 500;\n color: #999;\n}\n\n.dashboard-state-widget-prompt .title {\n font-size: 32px;\n font-weight: 500;\n color: #999;\n}\n\n.dashboard-state-widget-prompt .subtitle {\n font-size: 24px;\n font-weight: normal;\n color: #999;\n text-align: center;\n}", + "controllerScript": "self.onInit = function() {\n var $injector = self.ctx.$scope.$injector;\n self.ctx.$scope.stateId = self.ctx.settings.stateId || \"\";\n self.ctx.$scope.defaultAutofillLayout = self.ctx.settings.defaultAutofillLayout;\n self.ctx.$scope.defaultMargin = self.ctx.settings.defaultMargin;\n self.ctx.$scope.defaultBackgroundColor = self.ctx.settings.defaultBackgroundColor;\n self.ctx.$scope.syncParentStateParams = self.ctx.settings.syncParentStateParams !== false;\n}\n\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-dashboard-state-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"syncParentStateParams\":true,\"defaultAutofillLayout\":true,\"defaultMargin\":0,\"defaultBackgroundColor\":\"#fff\"},\"title\":\"Dashboard state widget\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"showLegend\":false}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/dashboards.json b/application/src/main/data/json/system/widget_types/dashboards.json new file mode 100644 index 0000000000..05529a856f --- /dev/null +++ b/application/src/main/data/json/system/widget_types/dashboards.json @@ -0,0 +1,21 @@ +{ + "fqn": "home_page_widgets.dashboards", + "name": "Dashboards", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Display and manage the 'last viewed' or 'starred' dashboards.", + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Dashboards\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/date_range_navigator.json b/application/src/main/data/json/system/widget_types/date_range_navigator.json new file mode 100644 index 0000000000..005fc0818a --- /dev/null +++ b/application/src/main/data/json/system/widget_types/date_range_navigator.json @@ -0,0 +1,21 @@ +{ + "fqn": "date.date_range_navigator", + "name": "Date-range-navigator", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to change the data range for other widgets on the dashboard.", + "descriptor": { + "type": "static", + "sizeX": 5, + "sizeY": 5.5, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-date-range-navigator-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"defaultInterval\":\"week\",\"stepSize\":\"day\"},\"title\":\"Date-range-navigator\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/device_admin_table.json b/application/src/main/data/json/system/widget_types/device_admin_table.json new file mode 100644 index 0000000000..1aff2917b5 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/device_admin_table.json @@ -0,0 +1,22 @@ +{ + "fqn": "entity_admin_widgets.device_admin_table", + "name": "Device admin table", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Customized entity table widget with preconfigured actions to create, update and delete devices.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-entities-table-widget-settings", + "dataKeySettingsDirective": "tb-entities-table-key-settings", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityLabel\":false,\"useRowStyleFunction\":false},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Add device

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Device name\\n \\n \\n Device name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\\n}\\n\\nfunction AddDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.addDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addDeviceFormGroup.markAsPristine();\\n let device = {\\n name: vm.addDeviceFormGroup.get('deviceName').value,\\n type: vm.addDeviceFormGroup.get('deviceType').value,\\n label: vm.addDeviceFormGroup.get('deviceLabel').value\\n };\\n deviceService.saveDevice(device).subscribe(\\n function (device) {\\n saveAttributes(device.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit device\",\"icon\":\"edit\",\"useShowWidgetActionFunction\":null,\"showWidgetActionFunction\":\"return true;\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Edit device

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Device name\\n \\n \\n Device name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\\n}\\n\\nfunction EditDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.device = null;\\n vm.attributes = {};\\n \\n vm.editDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editDeviceFormGroup.markAsPristine();\\n if (vm.editDeviceFormGroup.get('deviceType').value !== vm.device.type) {\\n delete vm.device.deviceProfileId;\\n }\\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value,\\n vm.device.type = vm.editDeviceFormGroup.get('deviceType').value,\\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value\\n deviceService.saveDevice(vm.device).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n deviceService.getDevice(entityId.id).subscribe(\\n function (device) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.device = device;\\n vm.editDeviceFormGroup.patchValue(\\n {\\n deviceName: vm.device.name,\\n deviceType: vm.device.type,\\n deviceLabel: vm.device.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"openInSeparateDialog\":false,\"openInPopover\":false,\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteDevice();\\n }\\n }\\n );\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/device_claiming_widget.json b/application/src/main/data/json/system/widget_types/device_claiming_widget.json new file mode 100644 index 0000000000..4de31ce0bb --- /dev/null +++ b/application/src/main/data/json/system/widget_types/device_claiming_widget.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.device_claiming_widget", + "name": "Device claiming widget", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to claim the device using name and optional secret key.", + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 4.5, + "resources": [], + "templateHtml": "
\n
\n \n {{deviceLabel}}\n \n \n {{requiredErrorDevice}}\n \n \n \n {{secretKeyLabel}}\n \n \n {{requiredErrorSecretKey}}\n \n \n
\n
\n \n
\n
\n", + "templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n", + "controllerScript": "let $scope;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n}\n\nfunction init() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n let $translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n let deviceService = $scope.$injector.get(self.ctx.servicesMap.get('deviceService'));\n let settings = self.ctx.settings || {};\n \n $scope.toastTargetId = 'device-claiming-widget' + utils.guid();\n $scope.secretKeyField = settings.deviceSecret;\n $scope.showLabel = settings.showLabel;\n\n let titleTemplate = \"\";\n let successfulClaim = utils.customTranslation(settings.successfulClaimDevice, settings.successfulClaimDevice) || $translate.instant('widgets.input-widgets.claim-successful');\n let failedClaimDevice = utils.customTranslation(settings.failedClaimDevice, settings.failedClaimDevice) || $translate.instant('widgets.input-widgets.claim-failed');\n let deviceNotFound = utils.customTranslation(settings.deviceNotFound, settings.deviceNotFound) || $translate.instant('widgets.input-widgets.claim-not-found');\n \n if (settings.widgetTitle && settings.widgetTitle.length) {\n titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n titleTemplate = self.ctx.widgetConfig.title;\n }\n self.ctx.widgetTitle = titleTemplate;\n \n $scope.deviceLabel = utils.customTranslation(settings.deviceLabel, settings.deviceLabel) || $translate.instant('widgets.input-widgets.device-name');\n $scope.requiredErrorDevice= utils.customTranslation(settings.requiredErrorDevice, settings.requiredErrorDevice) || $translate.instant('widgets.input-widgets.device-name-required');\n \n $scope.secretKeyLabel = utils.customTranslation(settings.secretKeyLabel, settings.secretKeyLabel) || $translate.instant('widgets.input-widgets.secret-key');\n $scope.requiredErrorSecretKey= utils.customTranslation(settings.requiredErrorSecretKey, settings.requiredErrorSecretKey) || $translate.instant('widgets.input-widgets.secret-key-required');\n \n $scope.labelClaimButon = utils.customTranslation(settings.labelClaimButon, settings.labelClaimButon) || $translate.instant('widgets.input-widgets.claim-device');\n \n $scope.claimDeviceFormGroup = $scope.fb.group(\n {deviceName: ['', [$scope.validators.required]]}\n );\n if ($scope.secretKeyField) {\n $scope.claimDeviceFormGroup.addControl('deviceSecret', $scope.fb.control('', [$scope.validators.required]));\n }\n \n $scope.claim = function(claimDeviceForm) {\n $scope.loading = true;\n\n let deviceName = $scope.claimDeviceFormGroup.get('deviceName').value;\n let claimRequest = {};\n if ($scope.secretKeyField) {\n claimRequest.secretKey = $scope.claimDeviceFormGroup.get('deviceSecret').value;\n }\n deviceService.claimDevice(deviceName, claimRequest, { ignoreErrors: true }).subscribe(\n function (data) {\n successClaim(claimDeviceForm);\n self.ctx.detectChanges();\n },\n function (error) {\n $scope.loading = false;\n if(error.status == 404) {\n $scope.showErrorToast(deviceNotFound, 'bottom', 'left', $scope.toastTargetId);\n } else {\n let errorMessage = failedClaimDevice;\n if (error.status !== 400) {\n if (error.error && error.error.message) {\n errorMessage = error.error.message;\n }\n }\n $scope.showErrorToast(errorMessage, 'bottom', 'left', $scope.toastTargetId);\n } \n self.ctx.detectChanges();\n }\n );\n }\n\n function successClaim(claimDeviceForm) {\n let deviceObj = {\n deviceName: ''\n };\n if ($scope.secretKeyField) {\n deviceObj.deviceSecret = '';\n } \n claimDeviceForm.resetForm(); \n $scope.claimDeviceFormGroup.reset(deviceObj);\n $scope.loading = false;\n $scope.showSuccessToast(successfulClaim, 2000, 'bottom', 'left', $scope.toastTargetId);\n self.ctx.updateAliases();\n }\n \n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-device-claiming-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"deviceSecret\":true,\"showLabel\":true},\"title\":\"Device claiming widget\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/digital_horizontal_bar.json b/application/src/main/data/json/system/widget_types/digital_horizontal_bar.json new file mode 100644 index 0000000000..d1eeacf49c --- /dev/null +++ b/application/src/main/data/json/system/widget_types/digital_horizontal_bar.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.digital_bar", + "name": "Digital horizontal bar", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display any value reading as a horizontal bar. Allows to configure value range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 6, + "sizeY": 2.5, + "resources": [], + "templateHtml": "", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 80) {\\n\\tvalue = 80;\\n} else if (value > 160) {\\n\\tvalue = 160;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"horizontalBar\",\"showTitle\":false,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital horizontal bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/digital_speedometer.json b/application/src/main/data/json/system/widget_types/digital_speedometer.json new file mode 100644 index 0000000000..3c5dded442 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/digital_speedometer.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.digital_speedometer", + "name": "Digital speedometer", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display speed. Allows to configure speed range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 5, + "sizeY": 3, + "resources": [], + "templateHtml": "", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 45) {\\n\\tvalue = 45;\\n} else if (value > 130) {\\n\\tvalue = 130;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"arc\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital speedometer\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/digital_thermometer.json b/application/src/main/data/json/system/widget_types/digital_thermometer.json new file mode 100644 index 0000000000..e8009a602d --- /dev/null +++ b/application/src/main/data/json/system/widget_types/digital_thermometer.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.digital_thermometer", + "name": "Digital thermometer", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display temperature. Allows to configure temperature range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 3, + "sizeY": 3, + "resources": [], + "templateHtml": "", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < -60) {\\n\\tvalue = 60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[\"#304ffe\",\"#7e57c2\",\"#ff4081\",\"#d32f2f\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"dashThickness\":1.5,\"minValue\":-60,\"gaugeColor\":\"#333333\",\"neonGlowBrightness\":35,\"gaugeType\":\"donut\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital thermometer\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/digital_vertical_bar.json b/application/src/main/data/json/system/widget_types/digital_vertical_bar.json new file mode 100644 index 0000000000..d21916864c --- /dev/null +++ b/application/src/main/data/json/system/widget_types/digital_vertical_bar.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.digital_vertical_bar", + "name": "Digital vertical bar", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display any value reading as a vertical bar. Allows to configure value range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 2.5, + "sizeY": 4.5, + "resources": [], + "templateHtml": "", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#3d5afe\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":14},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":8,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#cccccc\"},\"neonGlowBrightness\":20,\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"verticalBar\",\"showTitle\":false,\"minValue\":-60,\"dashThickness\":1.2,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital vertical bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/documentation_links.json b/application/src/main/data/json/system/widget_types/documentation_links.json new file mode 100644 index 0000000000..d27d167171 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/documentation_links.json @@ -0,0 +1,21 @@ +{ + "fqn": "home_page_widgets.documentation_links", + "name": "Documentation links", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Configure and display links to documentation and any other resources.", + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "\n\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-doc-links-widget-settings", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Documentation links\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/doughnut.json b/application/src/main/data/json/system/widget_types/doughnut.json new file mode 100644 index 0000000000..38ef1c6b71 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/doughnut.json @@ -0,0 +1,25 @@ +{ + "fqn": "charts.doughnut_chart_js", + "name": "Doughnut", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays latest values of the attributes or timeseries data for multiple entities in a doughnut chart. Supports numeric values only.", + "descriptor": { + "type": "latest", + "sizeX": 7, + "sizeY": 5, + "resources": [ + { + "url": "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js" + } + ], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n var borderColor = self.ctx.settings.borderColor || '#fff';\n var borderWidth = typeof self.ctx.settings.borderWidth !== 'undefined' ? self.ctx.settings.borderWidth : 5;\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(borderColor);\n dataset.borderWidth.push(borderWidth);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var options = {\n responsive: false,\n maintainAspectRatio: false,\n legend: {\n display: true,\n labels: {\n fontColor: '#666'\n }\n },\n tooltips: {\n callbacks: {\n label: function(tooltipItem, data) {\n var label = data.labels[tooltipItem.index];\n var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];\n var content = label + ': ' + value;\n var units = self.ctx.settings.units ? self.ctx.settings.units : self.ctx.units;\n if (units) {\n content += ' ' + units;\n } \n return content;\n }\n }\n }\n };\n\n if (self.ctx.settings.legend) {\n options.legend.display = self.ctx.settings.legend.display !== false;\n options.legend.labels.fontColor = self.ctx.settings.legend.labelsFontColor || '#666';\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'doughnut',\n data: pieData,\n options: options\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-doughnut-chart-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#26a69a\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#afb42b\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#673ab7\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"borderWidth\":5,\"borderColor\":\"#fff\",\"legend\":{\"display\":true,\"labelsFontColor\":\"#666666\"}},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/edge_quick_overview.json b/application/src/main/data/json/system/widget_types/edge_quick_overview.json new file mode 100644 index 0000000000..9ff5a56693 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/edge_quick_overview.json @@ -0,0 +1,21 @@ +{ + "fqn": "edge_widgets.edges_overview", + "name": "Edge Quick Overview", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Overview of entities related to ThingsBoard Edge.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.onDestroy = function() {\n};\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-edge-quick-overview-widget-settings", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"showTitleIcon\":true,\"titleIcon\":\"router\",\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Edge Quick Overview\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"widgetStyle\":{},\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/entities_hierarchy.json b/application/src/main/data/json/system/widget_types/entities_hierarchy.json new file mode 100644 index 0000000000..b272cde176 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/entities_hierarchy.json @@ -0,0 +1,21 @@ +{ + "fqn": "cards.entities_hierarchy", + "name": "Entities hierarchy", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays hierarchy of entities based on their relations. The root of the hierarchy is defined using entity alias. By default, displays entities related using \"Contains\" relation. You may change the behaviour using advanced settings.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesHierarchyWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-entities-hierarchy-widget-settings", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"var entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: \\\"FROM\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n/**\\n\\n// Function should return relations query object for current node used to fetch entity children.\\n// Function can return 'default' string value. In this case default relations query will be used.\\n\\n// The following example code will construct simple relations query that will fetch relations of type 'Contains'\\n// from the current entity.\\n\\nvar entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: \\\"FROM\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n**/\\n\",\"nodeHasChildrenFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node has children (whether it can be expanded).\\n\\n// The following example code will restrict entities hierarchy expansion up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n// The next example code will restrict entities expansion according to the value of example 'nodeHasChildren' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) {\\n return data['nodeHasChildren'] === 'true';\\n} else {\\n return true;\\n}\\n \\n**/\\n \",\"nodeOpenedFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be opened (expanded) when it first loaded.\\n\\n// The following example code will open by default nodes up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n**/\\n \",\"nodeDisabledFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be disabled (not selectable).\\n\\n// The following example code will disable current node according to the value of example 'nodeDisabled' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) {\\n return data['nodeDisabled'] === 'true';\\n} else {\\n return false;\\n}\\n \\n**/\\n\",\"nodeIconFunction\":\"/** \\n\\n// Function should return node icon info object.\\n// Resulting object should contain either 'materialIcon' or 'iconUrl' property. \\n// Where:\\n - 'materialIcon' - name of the material icon to be used from the Material Icons Library (https://material.io/tools/icons);\\n - 'iconUrl' - url of the external image to be used as node icon.\\n// Function can return 'default' string value. In this case default icons according to entity type will be used.\\n\\n// The following example code shows how to use external image for devices which name starts with 'Test' and use \\n// default icons for the rest of entities.\\n\\nvar entity = nodeCtx.entity;\\nif (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) {\\n return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'};\\n} else {\\n return 'default';\\n}\\n \\n**/\",\"nodeTextFunction\":\"/**\\n\\n// Function should return text (can be HTML code) for the current node.\\n\\n// The following example code will generate node text consisting of entity name and temperature if temperature value is present in entity attributes/timeseries.\\n\\nvar data = nodeCtx.data;\\nvar entity = nodeCtx.entity;\\nvar text = entity.name;\\nif (data.hasOwnProperty('temperature') && data['temperature'] !== null) {\\n text += \\\" \\\"+ data['temperature'] +\\\" °C\\\";\\n}\\nreturn text;\\n\\n**/\",\"nodesSortFunction\":\"/**\\n\\n// This function is used to sort nodes of the same level. Function should compare two nodes and return \\n// integer value: \\n// - less than 0 - sort nodeCtx1 to an index lower than nodeCtx2\\n// - 0 - leave nodeCtx1 and nodeCtx2 unchanged with respect to each other\\n// - greater than 0 - sort nodeCtx2 to an index lower than nodeCtx1\\n\\n// The following example code will sort entities first by entity type in alphabetical order then\\n// by entity name in alphabetical order.\\n\\nvar result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);\\nif (result === 0) {\\n result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);\\n}\\nreturn result;\\n \\n**/\"},\"title\":\"Entities hierarchy\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"widgetStyle\":{},\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/entities_table.json b/application/src/main/data/json/system/widget_types/entities_table.json new file mode 100644 index 0000000000..69bd1475dd --- /dev/null +++ b/application/src/main/data/json/system/widget_types/entities_table.json @@ -0,0 +1,24 @@ +{ + "fqn": "cards.entities_table", + "name": "Entities table", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays list of entities that match selected alias and filter with ability of additional full text search and pagination. Highly customizable using widget styles, data source keys and widget actions.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-entities-table-widget-settings", + "dataKeySettingsDirective": "tb-entities-table-key-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-entities-table-basic-config", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/gateway_configuration.json b/application/src/main/data/json/system/widget_types/gateway_configuration.json new file mode 100644 index 0000000000..6e94a2f6ce --- /dev/null +++ b/application/src/main/data/json/system/widget_types/gateway_configuration.json @@ -0,0 +1,21 @@ +{ + "fqn": "gateway_widgets.gateway_configuration", + "name": "Gateway Configuration", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to define gateway configuration for a single selected device.", + "descriptor": { + "type": "static", + "sizeX": 8, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gateway-config-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"Gateway Configuration\",\"archiveFileName\":\"configurationGateway\"},\"title\":\"Gateway Configuration\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json b/application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json new file mode 100644 index 0000000000..ce1cb04a66 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json @@ -0,0 +1,21 @@ +{ + "fqn": "gateway_widgets.config_form_latest", + "name": "Gateway configuration (Single device)", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to create or choose the gateway device and edit the configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 9, + "resources": [], + "templateHtml": "\n", + "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", + "controllerScript": "self.onInit = function() {\n}\n\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\t\t\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gateway-config-single-device-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gatewayTitle\":\"Gateway configuration (Single device)\"},\"title\":\"Gateway configuration (Single device)\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/gateway_events.json b/application/src/main/data/json/system/widget_types/gateway_events.json new file mode 100644 index 0000000000..5c2a38edfe --- /dev/null +++ b/application/src/main/data/json/system/widget_types/gateway_events.json @@ -0,0 +1,21 @@ +{ + "fqn": "gateway_widgets.attributes_card", + "name": "Gateway events", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to browse events from the gateway.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 8, + "resources": [], + "templateHtml": "", + "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", + "controllerScript": "let types;\nlet eventsReg = \"eventsReg\";\n\nself.onInit = function() {\n \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n\n if (self.ctx.datasources.length && self.ctx.datasources[0].type === 'entity') {\n getDatasourceKeys(self.ctx.datasources[0]);\n } else {\n processDatasources(self.ctx.datasources);\n }\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, false);\n }\n else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nfunction processDatasources(datasources) {\n var i = 0;\n var tbDatasource = datasources[i];\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n self.onResize();\n}\n\nfunction getDatasourceKeys (datasource) {\n let entityService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('entityService'));\n if (datasource.entityId && datasource.entityType) {\n entityService.getEntityKeys({entityType: datasource.entityType, id: datasource.entityId}, '', 'timeseries').subscribe(\n function(data){\n if (data.length) {\n subscribeForKeys (datasource, data);\n }\n });\n }\n}\n\nfunction subscribeForKeys (datasource, data) {\n let eventsRegVals = self.ctx.settings[eventsReg];\n if (eventsRegVals && eventsRegVals.length > 0) {\n var dataKeys = [];\n data.sort();\n data.forEach(dataValue => {eventsRegVals.forEach(event => {\n if (dataValue.toLowerCase().includes(event.toLowerCase())) {\n var dataKey = {\n type: 'timeseries',\n name: dataValue,\n label: dataValue,\n settings: {},\n _hash: Math.random()\n };\n dataKeys.push(dataKey);\n }\n })});\n\n if (dataKeys.length) {\n updateSubscription (datasource, dataKeys);\n }\n }\n}\n\nfunction updateSubscription (datasource, dataKeys) {\n var datasources = [\n {\n type: 'entity',\n name: datasource.aliasName,\n aliasName: datasource.aliasName,\n entityAliasId: datasource.entityAliasId,\n dataKeys: dataKeys\n }\n ];\n \n var subscriptionOptions = {\n datasources: datasources,\n useDashboardTimewindow: false,\n type: 'latest',\n callbacks: {\n onDataUpdated: (subscription) => {\n self.ctx.data = subscription.data;\n self.onDataUpdated();\n }\n }\n };\n \n processDatasources(datasources);\n self.ctx.subscriptionApi.createSubscription(subscriptionOptions, true).subscribe(\n (subscription) => {\n self.ctx.defaultSubscription = subscription;\n }\n );\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gateway-events-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Function Math.round\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.826503672916844,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"eventsTitle\":\"Gateway Events Form\",\"eventsReg\":[]},\"title\":\"Gateway events\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/gauge.json b/application/src/main/data/json/system/widget_types/gauge.json new file mode 100644 index 0000000000..3ac6fd00b4 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/gauge.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.gauge_justgage", + "name": "Gauge", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display any value reading. Allows to configure value range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 4, + "sizeY": 3, + "resources": [], + "templateHtml": "", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":36,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"arc\"},\"title\":\"Gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/getting_started.json b/application/src/main/data/json/system/widget_types/getting_started.json new file mode 100644 index 0000000000..65aaa3e167 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/getting_started.json @@ -0,0 +1,21 @@ +{ + "fqn": "home_page_widgets.getting_started", + "name": "Getting started", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Getting started instruction.", + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Getting started\",\"dropShadow\":true}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/google_map.json b/application/src/main/data/json/system/widget_types/google_map.json new file mode 100644 index 0000000000..2ef3dcceba --- /dev/null +++ b/application/src/main/data/json/system/widget_types/google_map.json @@ -0,0 +1,21 @@ +{ + "fqn": "maps_v2.google_maps", + "name": "Google Map", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Show latest values and location of the entities on Google Maps.", + "descriptor": { + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], + "templateHtml": "", + "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"google-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7568\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"Google Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/here_map.json b/application/src/main/data/json/system/widget_types/here_map.json new file mode 100644 index 0000000000..eac0fc751d --- /dev/null +++ b/application/src/main/data/json/system/widget_types/here_map.json @@ -0,0 +1,21 @@ +{ + "fqn": "maps_v2.here_map", + "name": "HERE Map", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Show latest values and location of the entities on HERE maps.", + "descriptor": { + "type": "latest", + "sizeX": 9.5, + "sizeY": 6, + "resources": [], + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('here', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"here\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"HERE.normalDay\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"useV3\":true,\"apiKey\":\"kVXykxAfZ6LS4EbCTO02soFVfjA7HoBzNVVH9u7nzoE\"},\"mapImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.5,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"HERE Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/horizontal_bar.json b/application/src/main/data/json/system/widget_types/horizontal_bar.json new file mode 100644 index 0000000000..de72fc84a0 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/horizontal_bar.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.horizontal_bar_justgage", + "name": "Horizontal bar", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABp1BMVEVmZmZnZ2doaGhpaWlqampra2tsbGxtbW1ubm5vb29wcHBxcXFycnJzc3N0dHR1dXV2dnZ3d3d4eHh5eXl7e3t8fHx9fX1/f3+AgICCgoKDg4OFhYWGhoaIiIiKioqLi4uMjIyOjo6Pj4+RkZGSkpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqbm5ucnJydnZ2enp6goKChoaGioqKjo6OkpKSlpaWmpqanp6eoqKipqamqqqqrq6usrKytra2urq6wsLCxsbGysrKzs7O0tLS1tbW2tra3t7e4uLi5ubm6urq7u7u8vLy9vb2+vr7AwMDBwcHCwsLDw8PExMTFxcXGxsbHx8fIyMjJycnKysrLy8vMzMzNzc3Ozs7Pz8/Q0NDR0dHS0tLT09PU1NTV1dXW1tbX19fY2NjZ2dnc3Nzd3d3f39/g4ODh4eHi4uLj4+Pk5OTl5eXm5ubn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fHy8vLz8/P0Qzb09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7////7etFxAAAAAWJLR0SMbAvSQwAABdlJREFUeNrt3PtfE2cWx/FPuGgNi1S7LrpgVyi92AMKrqsWNRUd0IpaxBapEKC0ARSUi1gsVKmpMbfvH70/zOT6q1Jn2uf8NM+ZeZHzJjPznIThQW8d2URN5DRdGXjSYiKRmCwf/DiRSIxJmvX3X70581LvJHj7H/HGaiKnscrgjJQ0s9PZ0sHXzeyqpHvlQ3rvF0MCyXme53nnzC54nud5eY2VNj3vpg+xxeDYdF8Fct7zvKEBMxsPCcSP782mgs0xs2RlR9KC2iVp1iqQB5JUeGBmW1GBnDF74Q8u2plaiDRsNhkVyHip6mdm4/WQGbPRqEAWL9i5oiSNm63XQ6bMbocX8k0qlUqlUss+5OdpszVJ+QEbelEHKVw2mwkvJIgrPmRut9fuSHpktvhrGTKRyWR2Vz2zU7tRgUxr2E6/kUasP/usfh4xWwjx7fdBJpPJZDJZHzKph2YLSvfamNbrIV+vKcSQmZqL/TvlB2xIs2ZP9bgMuZ5KpZZWdt/R6/8pkLvSd2a/DdqgtFx/14oYZMts2Gw2+hBdNrO+dOQgIzNBlCHzZnZDkYOUo1iCZE6ZLf8VIBq1/xaiAHnf4SAO4iAO4iAO4iAO4iAO4iAO4iDhhKyPDvadvfZjpia5MzHY3z84sRMdSOoYfsSHcuXkq/5GP9nY/yoikIsxytH5R5B8friSPLQVCcgNAGKH4gB84ifz7QAcaAOgPRcByE4T0PpDVlrpAJiXJI0BnNiQNo4DjEUAcgVofCJJ2m0BTkuSuoBDryXpZRzoigDkU+DzYHsAOCJJagEGg0sIiEcAcjuRSMwF29eBA5JUjAHf+slxIFaI1oQ4XH5HDgLX/eQo0Bqxmb0L6JMkdQNf+sk+oDtakAkgtipJmgEaFiVpqQGYjQokvbW1MtkbA4KnUIongebz9ycvNANWjApkOJjDG6+VMtkvKzN7b1YRg8SqHs7Id5ccPQVFDQJdwWNBetJeeUf+vRkZyMbU1NStnhhwxG91n8WBNu/HuaEW4KOdaN21fm4G/idJ+hg4uitJO0ci0qJUhwc0v5b0CGDZTy4BrEQL8hRgSdIt4MNS9iBwJ/SQYjqdTueDQQZgWtIFoLN0SEelgQwx5DVA6S/P2wApSYPlpkvSP4GL4T+14uUL3O9ReB40irFf/ORWDLgdfkgf0LTkf1g8DByVpA2A7pwk5XsANsMPedwANH21sPFwpLVymn0KcOz71dX7HQCfReH2O0x1DPj94fO26mTb80jMI6NN5Ypjl0p91cbRiqN9PSItyubZAwDs712uJLN3AsrRO3vQ/O7VhFhcm5+af1Lf5b5cTE4vvtybV3RfYjuIgziIgziIgziIgziIgzjI+4b8/rax/b7CQRzEQRzEQRzEQRzEQRzEQRzEQRzEQRzEQRzEQRzEQRzEQRzEQRzEQRzEQRzEQRzEQRzEQf4mEPfgmYM4iIM4iIM4iIM4iIP8TSDzn8SJ9/wU3iofnbtR2qytdbXvYGz/8fGiDxkK/l9+OJyKX4f/BZeCQW2tyQZ/dLIgpCVovnR3sJHYwxAykidiUIbU1vriA2Kn7l5rgdtC+gzuSfoWToYQ8h+ItZUhtbVeg7OSVuBwEeWaiGclZfbRXAghpHN0e6wEqau1E1YlqQOeoDX4WJLUCU/DB3klqQyprbW4j6aiJJ2HCZQqLdfX469XFMIoQ2pr/QM+kiRdhW/QDPRLknphLuSQ6lpntQPtkqRRuIKmSzv79mLVwXcLqa51Rtt/UUiyamfYT61kzS+9DvJT6QL6AhZCDqmtNQ2HJUkjcAOtwAlJ0nFYCzmkutZVFRrZJ0n6Cu6hNw20FiQV/kFjLuSQqlobctIxeCZJ3fAI6YR/bcztxZqv7xhSVWuXpMv+Uqk7DbTkkabg4IPN6Q8hGXpIba2bTTTderrQAQnhzy7+WqkKPaSu1pvB6NhrIakw0gq0evkIQOpq/eEIsP/sq9JH3eLm8mZB0Yi6WreX17OS9H8TXZLm48kP1gAAAABJRU5ErkJggg==", + "description": "Preconfigured gauge to display any value reading as a horizontal bar. Allows to configure value range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 7, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"horizontalBar\"},\"title\":\"Horizontal bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/horizontal_value_card.json b/application/src/main/data/json/system/widget_types/horizontal_value_card.json new file mode 100644 index 0000000000..46e76a19bc --- /dev/null +++ b/application/src/main/data/json/system/widget_types/horizontal_value_card.json @@ -0,0 +1,23 @@ +{ + "fqn": "cards.horizontal_value_card", + "name": "Horizontal value card", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Designed to display single value of the selected attribute or timeseries data. Widget styles are customizable.", + "descriptor": { + "type": "latest", + "sizeX": 5, + "sizeY": 1.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '130px',\n embedTitlePanel: true\n };\n};\n\nself.onDestroy = function() {\n};\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-value-card-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-value-card-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"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';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"constant\",\"color\":\"#5469FF\",\"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';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"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';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"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';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/html_card.json b/application/src/main/data/json/system/widget_types/html_card.json new file mode 100644 index 0000000000..b4378f89d4 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/html_card.json @@ -0,0 +1,21 @@ +{ + "fqn": "cards.html_card", + "name": "HTML Card", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAATFSURBVHja7d3tUxNXFMdx/u7vEkAJsKQK6bilKaBV1Eh5GGNaSyq0WodhRuqMtQJqW5UHCwKGMRJiQpL99cWGEGaaTjtjgaTnvNpz7mYnn8neu/duXtwWFbdWlxs8Vrf21VJcy5TU4FHKrBVbtjJqgshstayWmgFSWm1ZVlPEskEMYhCDGMQgBjGIQQxiEIMYxCAGMYhBDGIQgxjEIAYxiEEMYhCDGMQgBjGIQQxiEIMYxCAG+T9ACrlcLjjyc7lcWflcbfhBfnByOZfL5cpSKZfL+f/2G+WAJ/8dJAHh4GgFeK4BaiOjMYDtyskzAIvSQ+BtI0JmKif3NRBk3PM8Lwwhz/M8LxtAzgf30RoNBJEkjUOk0j4GwJok6XaDQ8LwtSSVXVrP1of4v1w978bu5YNh4fHlfnfwfpDo15Hevm/eHkCyP8Tc/rGV44d8FaLbl/QCRrrqQopXg251bkdSdjBIIpuSNAtAbwXyqgsA595Hh3Sm0+l0Or1QDzJxLWiYhCftdSFJoKMHiPnyvwTOhIHeD9KGA3SfIYBkOsGJtIPz/GNDauIvIfEFGJdKnZwtOPUgOw4kfT0E1vQMmPb12IEZaRJCz1RKBpBJCG+ocAVixw25XArTUdACTBbqdvY56MhLisK8xisDXRyiUi+MStoDnqjUCT9Keg3Oh48MCcXj8Xg8PlwPMqQE/Kzr8HK3LiQBFyozhaI8uClJ8+AUi8CD6qi1DYymUqkksH7Mnf0zLcNIvo2In64LGYXhauLCHUlaAnb3gIUq5FXN7//qmCH98s8RmoXv9KYuZBI+ryZ9cFsKzsvngUdVyAowcDGIjWOGRKRpaINNrdWF3IUeX9LS/PymLsMlSZqCLikM01XIe+CnE3ogRqRtAE9arQtZCVqynbCoWXBWpfddMCaNgLsnLQSj1gVwM5Je3/RPAKIBYPYIJOp5nud5dw8uFIPQxO1eCOf1oRs6Jm71QOtG0FMiydFQAHkKnJlIjTgkTwIyB87uEUglbh5c6G13UGhdkvSyPUicB5J0HYBQe/Bkv+tUPnr/JCDZVq7obyF6d6MNnC+COdTmSCs4A78Hs5dkG3zyYqAy13oec8CJ/XZ6l7qFzfW9apJ/80dNsr5V2yNyGxt7tmY3iEEMYhCDGOTEIJnD6YtBDGKQpocsDrvR5K4kyX96PeoOzu5Lysbj8b2Zvv7SkepphgwBcD4vqRAPVlXR91IauASUVAhWgny6e7ohtEdCwcpdCaDTBYYDCED5sHrxdEOuFZRx4Ya06cC3ZS05sKI04EzM3demA1NlLTqweuo7ewI86U7lHdYQzCgNzEnSHXB9SYPw/amHpKBPGoFzqVQqFYUxpYEX0pHqeINAav4wvXoI+eyweq1BIDHorry+nT6E1FRnGgQyCtFqUxVyo7baGJBHlVG4mFivgRxU9xMbjQIpR4HBqYRLz84hpBwFhqYSLu5Og0C07VZ6dXTvEHKk2iAQZZOdQHcqX3NrSdlb1WrjLHXL26/T/j+s2prdIAYxiEEMYhCDGMQgBjGIQQxiEIMYxCAGMYhBDGIQgxjEIAYxiEEMYhCDGMQgBjGIQQxiEIMY5PRAmmaD4ObYsvndVst+c2yiXWppjm3NS/oTe0OjFEeU1MMAAAAASUVORK5CYII=", + "description": "Useful to inject custom HTML code. Designed to display static information only.", + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n var $injector = self.ctx.$scope.$injector;\n var utils = $injector.get(self.ctx.servicesMap.get('utils'));\n\n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n var evtFnPrefix = 'htmlCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml + self.ctx.widget.id));\n cardHtml = '
' + \n self.ctx.settings.cardHtml + \n '
';\n cardHtml = replaceCustomTranslations(cardHtml);\n self.ctx.$container.html(cardHtml);\n\n window[evtFnPrefix + '_onClickFn'] = function (event) {\n self.ctx.actionsApi.elementClick(event);\n }\n\n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function replaceCustomTranslations (pattern) {\n var customTranslationRegex = new RegExp('{i18n:[^{}]+}', 'g');\n pattern = pattern.replace(customTranslationRegex, getTranslationText);\n return pattern;\n }\n \n function getTranslationText (variable) {\n return utils.customTranslation(variable, variable);\n \n }\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-html-card-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/html_value_card.json b/application/src/main/data/json/system/widget_types/html_value_card.json new file mode 100644 index 0000000000..efac2feec4 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/html_value_card.json @@ -0,0 +1,21 @@ +{ + "fqn": "cards.html_value_card", + "name": "HTML Value Card", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays configurable HTML with ability to inject values from the selected datasource. For example, display single or multiple attribute values.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n self.ctx.htmlSet = false;\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-value-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n var evtFnPrefix = 'htmlValueCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml + self.ctx.widget.id));\n self.ctx.html = '
' + \n self.ctx.settings.cardHtml + \n '
';\n\n self.ctx.replaceInfo = processHtmlPattern(self.ctx.html, self.ctx.data);\n \n updateHtml();\n \n window[evtFnPrefix + '_onClickFn'] = function (event) {\n self.ctx.actionsApi.elementClick(event);\n }\n\n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function processHtmlPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n if (label == 'entityName') {\n variableInfo.isEntityName = true;\n } else if (label == 'entityLabel') {\n variableInfo.isEntityLabel = true;\n } else if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (!variableInfo.isEntityName && !variableInfo.isEntityLabel && variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n}\n\nself.onDataUpdated = function() {\n updateHtml();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n singleEntity: true,\n dataKeysOptional: true\n };\n}\n\n\nself.onDestroy = function() {\n}\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateHtml() {\n var $injector = self.ctx.$scope.$injector;\n var utils = $injector.get(self.ctx.servicesMap.get('utils'));\n var text = self.ctx.html;\n var updated = false;\n for (var v in self.ctx.replaceInfo.variables) {\n var variableInfo = self.ctx.replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n } else if (variableInfo.isEntityName) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n } else if (variableInfo.isEntityLabel) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityLabel || self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n }\n if (typeof variableInfo.lastVal === undefined ||\n variableInfo.lastVal !== txtVal) {\n updated = true;\n variableInfo.lastVal = txtVal;\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !self.ctx.htmlSet) {\n text = replaceCustomTranslations(text);\n self.ctx.$container.html(text);\n if (!self.ctx.htmlSet) {\n self.ctx.htmlSet = true;\n }\n }\n \n function replaceCustomTranslations (pattern) {\n var customTranslationRegex = new RegExp('{i18n:[^{}]+}', 'g');\n pattern = pattern.replace(customTranslationRegex, getTranslationText);\n return pattern;\n }\n \n function getTranslationText (variable) {\n return utils.customTranslation(variable, variable);\n \n }\n}\n\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-html-card-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"My value\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.random() * 5.45;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"cardCss\":\".card {\\n width: 100%;\\n height: 100%;\\n border: 2px solid #ccc;\\n box-sizing: border-box;\\n}\\n\\n.card .content {\\n padding: 20px;\\n display: flex;\\n flex-direction: row;\\n align-items: center;\\n justify-content: space-around;\\n height: 100%;\\n box-sizing: border-box;\\n}\\n\\n.card .content .column {\\n display: flex;\\n flex-direction: column; \\n justify-content: space-around;\\n height: 100%;\\n}\\n\\n.card h1 {\\n text-transform: uppercase;\\n color: #999;\\n font-size: 20px;\\n font-weight: bold;\\n margin: 0;\\n padding-bottom: 10px;\\n line-height: 32px;\\n}\\n\\n.card .value {\\n font-size: 38px;\\n font-weight: 200;\\n}\\n\\n.card .description {\\n font-size: 20px;\\n color: #999;\\n}\\n\",\"cardHtml\":\"
\\n
\\n
\\n

Value title

\\n
\\n ${My value:2} units.\\n
\\n
\\n Value description text\\n
\\n
\\n \\n
\\n
\"},\"title\":\"HTML Value Card\",\"dropShadow\":false,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/image_map.json b/application/src/main/data/json/system/widget_types/image_map.json new file mode 100644 index 0000000000..a09794d68a --- /dev/null +++ b/application/src/main/data/json/system/widget_types/image_map.json @@ -0,0 +1,21 @@ +{ + "fqn": "maps_v2.image_map", + "name": "Image Map", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Show latest values and location of the entities on image map. Uses configurable background image and coordintates in range from 0 to 1.", + "descriptor": { + "type": "latest", + "sizeX": 8.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"image-map\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/knob_control.json b/application/src/main/data/json/system/widget_types/knob_control.json new file mode 100644 index 0000000000..83cf6d1c66 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/knob_control.json @@ -0,0 +1,21 @@ +{ + "fqn": "control_widgets.knob_control", + "name": "Knob Control", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Sends the command to device with specified value each time user changes the value. Uses 'setValue' and 'getValue' RPC calls by default. The name of the RPC calls is configurable in advanced settings.", + "descriptor": { + "type": "rpc", + "sizeX": 5, + "sizeY": 4.5, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-knob-control-widget-settings", + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"maxValue\":100,\"initialValue\":50,\"minValue\":0,\"title\":\"Knob control\",\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\"},\"title\":\"Knob Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/label_widget.json b/application/src/main/data/json/system/widget_types/label_widget.json new file mode 100644 index 0000000000..2a586faa3b --- /dev/null +++ b/application/src/main/data/json/system/widget_types/label_widget.json @@ -0,0 +1,21 @@ +{ + "fqn": "cards.label_widget", + "name": "Label widget", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays static image and multiple values of selected attributes or timeseries keys on top of it. Position of the values on the image is configurable using advanced settings.", + "descriptor": { + "type": "latest", + "sizeX": 4.5, + "sizeY": 5, + "resources": [], + "templateHtml": "", + "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n \n var imageUrl = self.ctx.settings.backgroundImageUrl ? self.ctx.settings.backgroundImageUrl :\n 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==';\n\n self.ctx.$container.css('background', 'url(\"'+imageUrl+'\") no-repeat');\n self.ctx.$container.css('backgroundSize', 'contain');\n self.ctx.$container.css('backgroundPosition', '50% 50%');\n \n function processLabelPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n var configuredLabels = self.ctx.settings.labels;\n if (!configuredLabels) {\n configuredLabels = [];\n }\n \n self.ctx.labels = [];\n\n for (var l = 0; l < configuredLabels.length; l++) {\n var labelConfig = configuredLabels[l];\n var localConfig = {};\n localConfig.font = {};\n \n localConfig.pattern = labelConfig.pattern ? labelConfig.pattern : '${#0}';\n localConfig.x = labelConfig.x ? labelConfig.x : 0;\n localConfig.y = labelConfig.y ? labelConfig.y : 0;\n localConfig.backgroundColor = labelConfig.backgroundColor ? labelConfig.backgroundColor : 'rgba(0,0,0,0)';\n \n var settingsFont = labelConfig.font;\n if (!settingsFont) {\n settingsFont = {};\n }\n \n localConfig.font.family = settingsFont.family || 'Roboto';\n localConfig.font.size = settingsFont.size ? settingsFont.size : 6;\n localConfig.font.style = settingsFont.style ? settingsFont.style : 'normal';\n localConfig.font.weight = settingsFont.weight ? settingsFont.weight : '500';\n localConfig.font.color = settingsFont.color ? settingsFont.color : '#fff';\n \n localConfig.replaceInfo = processLabelPattern(localConfig.pattern, self.ctx.data);\n \n var label = {};\n var labelElement = $('
');\n labelElement.css('position', 'absolute');\n labelElement.css('display', 'none');\n labelElement.css('top', '0');\n labelElement.css('left', '0');\n labelElement.css('backgroundColor', localConfig.backgroundColor);\n labelElement.css('color', localConfig.font.color);\n labelElement.css('fontFamily', localConfig.font.family);\n labelElement.css('fontStyle', localConfig.font.style);\n labelElement.css('fontWeight', localConfig.font.weight);\n \n labelElement.html(localConfig.pattern);\n self.ctx.$container.append(labelElement);\n label.element = labelElement;\n label.config = localConfig;\n label.htmlSet = false;\n label.visible = false;\n self.ctx.labels.push(label);\n }\n\n var bgImg = $('');\n bgImg.hide();\n bgImg.bind('load', function()\n {\n self.ctx.bImageHeight = $(this).height();\n self.ctx.bImageWidth = $(this).width();\n self.onResize();\n });\n self.ctx.$container.append(bgImg);\n bgImg.attr('src', imageUrl);\n \n self.onDataUpdated();\n}\n\nself.onDataUpdated = function() {\n updateLabels();\n}\n\nself.onResize = function() {\n if (self.ctx.bImageHeight && self.ctx.bImageWidth) {\n var backgroundRect = {};\n var imageRatio = self.ctx.bImageWidth / self.ctx.bImageHeight;\n var componentRatio = self.ctx.width / self.ctx.height;\n if (componentRatio >= imageRatio) {\n backgroundRect.top = 0;\n backgroundRect.bottom = 1.0;\n backgroundRect.xRatio = imageRatio / componentRatio;\n backgroundRect.yRatio = 1;\n var offset = (1 - backgroundRect.xRatio) / 2;\n backgroundRect.left = offset;\n backgroundRect.right = 1 - offset;\n } else {\n backgroundRect.left = 0;\n backgroundRect.right = 1.0;\n backgroundRect.xRatio = 1;\n backgroundRect.yRatio = componentRatio / imageRatio;\n var offset = (1 - backgroundRect.yRatio) / 2;\n backgroundRect.top = offset;\n backgroundRect.bottom = 1 - offset;\n }\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var labelLeft = backgroundRect.left*100 + (label.config.x*backgroundRect.xRatio);\n var labelTop = backgroundRect.top*100 + (label.config.y*backgroundRect.yRatio);\n var fontSize = self.ctx.height * backgroundRect.yRatio * label.config.font.size / 100;\n label.element.css('top', labelTop + '%');\n label.element.css('left', labelLeft + '%');\n label.element.css('fontSize', fontSize + 'px');\n if (!label.visible) {\n label.element.css('display', 'block');\n label.visible = true;\n }\n }\n } \n}\n\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateLabels() {\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var text = label.config.pattern;\n var replaceInfo = label.config.replaceInfo;\n var updated = false;\n for (var v = 0; v < replaceInfo.variables.length; v++) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n updated = true;\n } else {\n txtVal = val;\n updated = true;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !label.htmlSet) {\n label.element.html(text);\n if (!label.htmlSet) {\n label.htmlSet = true;\n }\n }\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n singleEntity: true\n };\n};\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-label-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"var\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"backgroundImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"labels\":[{\"pattern\":\"Value: ${#0:2} units.\",\"x\":20,\"y\":47,\"font\":{\"color\":\"#515151\",\"family\":\"Roboto\",\"size\":6,\"style\":\"normal\",\"weight\":\"500\"}}]},\"title\":\"Label widget\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/lcd_bar_gauge.json b/application/src/main/data/json/system/widget_types/lcd_bar_gauge.json new file mode 100644 index 0000000000..2771b26f58 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/lcd_bar_gauge.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.lcd_bar_gauge", + "name": "LCD bar gauge", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display any value reading as a bar. Allows to configure value range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 2, + "sizeY": 3.5, + "resources": [], + "templateHtml": "", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"400\",\"size\":16},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"verticalBar\",\"units\":\"%\"},\"title\":\"LCD bar gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/lcd_gauge.json b/application/src/main/data/json/system/widget_types/lcd_gauge.json new file mode 100644 index 0000000000..8a2ec33b4d --- /dev/null +++ b/application/src/main/data/json/system/widget_types/lcd_gauge.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.lcd_gauge", + "name": "LCD gauge", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display any value reading as an arc. Allows to configure value range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 5, + "sizeY": 3, + "resources": [], + "templateHtml": "", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 180) {\\n\\tvalue = 180;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"arc\"},\"title\":\"LCD gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/led_indicator.json b/application/src/main/data/json/system/widget_types/led_indicator.json new file mode 100644 index 0000000000..cdf0816ec3 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/led_indicator.json @@ -0,0 +1,21 @@ +{ + "fqn": "control_widgets.led_indicator", + "name": "Led indicator", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Visualize the state of the device. Fetches the value from device using RPC or using attribute subscription.", + "descriptor": { + "type": "rpc", + "sizeX": 2.5, + "sizeY": 2.5, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-led-indicator-widget-settings", + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":true,\"title\":\"Led indicator\",\"ledColor\":\"#4caf50\",\"valueAttribute\":\"value\",\"retrieveValueMethod\":\"attribute\",\"parseValueFunction\":\"return data ? true : false;\",\"performCheckStatus\":true,\"checkStatusMethod\":\"checkStatus\"},\"title\":\"Led indicator\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/markdown_html_card.json b/application/src/main/data/json/system/widget_types/markdown_html_card.json new file mode 100644 index 0000000000..23507dc415 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/markdown_html_card.json @@ -0,0 +1,21 @@ +{ + "fqn": "cards.markdown_card", + "name": "Markdown/HTML Card", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Renders markdown/HTML using configurable pattern or function with applied attributes or timeseries values.", + "descriptor": { + "type": "latest", + "sizeX": 5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-markdown-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Random}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"useMarkdownTextFunction\":false},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/markers_placement___google_maps.json b/application/src/main/data/json/system/widget_types/markers_placement___google_maps.json new file mode 100644 index 0000000000..5728553792 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/markers_placement___google_maps.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.markers_placement_google_maps", + "name": "Markers Placement - Google Maps", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows configuring location of the selected entities on the Google Map.", + "descriptor": { + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], + "templateHtml": "", + "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"colorFunction\":\"\\n\",\"color\":\"#fe7569\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"defaultZoomLevel\":5,\"provider\":\"google-map\",\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\",\"showPolygonTooltip\":false},\"title\":\"Markers Placement - Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"8d3c0156-0a14-7a6f-0ddd-0ec16b9ffc91\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"46bf69cd-8906-234c-a879-e2e4c92f5b67\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/markers_placement___image_map.json b/application/src/main/data/json/system/widget_types/markers_placement___image_map.json new file mode 100644 index 0000000000..ef4a71b16a --- /dev/null +++ b/application/src/main/data/json/system/widget_types/markers_placement___image_map.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.markers_placement_image_map", + "name": "Markers Placement - Image Map", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows configuring location of the selected entities on the image map.", + "descriptor": { + "type": "latest", + "sizeX": 8.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"showTooltipAction\":\"click\",\"defaultCenterPosition\":\"0,0\",\"provider\":\"image-map\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\"},\"title\":\"Markers Placement - Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"c39f512a-21c6-6b06-3aa1-715262c6553d\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"94bf5ffd-b526-c6c3-ae3b-ab42191217d9\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json b/application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json new file mode 100644 index 0000000000..4115c3f871 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.markers_placement_openstreetmap", + "name": "Markers Placement - OpenStreetMap", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAADAFBMVEXT8dGYzf3Y9dTc+dnU8dLW9NTV8dPU8tLyyUzR79DY9tXT79Hb+NfV89Pa9tfu8vTM6szN68zw9fbL6cvO7M3X89X////j9ePr8fLh7+Pb9tjO6c3y9/jQ7c7e7OHS7dEwVoDg7uLq7/Gdp57m9Ojh9OHc6d/j8eX7/vvX8tXo9+nW79Whq6HG5sf6/PrM5szk6uzc89v4/PfQ68/a5t3Z8dhvc33j6Orf9d68xbxgZHH1+vvI38fz+Pu6wLre8N9laXaLkpcWFxXe+dzG48fR29TN2dG/ycVXWmrR39TBzsDc7d3Z89fH0cvE4cXV69Xm7O6Qlpzp7u66xMG+yb73+/23vbh9gopcX20DAwN3fITy+PPU39lqbnnq9+zW49nH2cfz/POWnZ/M4Mtzd4Hl+OXF1cWZoKSdpaaIjZTv++5NUGDF2MXv8e/Z7drCzcfD08NTVmX3+Pfs+evl7efY59vJ3MjC0MK2w7yu1/vX7c6vt7crLCqzvLukrqWnrbCSmp6EiZFDRVYzNDHE4fvR1deg0fzg6uTK1s+BhY7i+ODU5NUiIyHb89miq6tJS1umsajP5s+bz/3K5PvO1M/Gycq6zeHa39yrs7N6f3rG3fK2uLZ6f4fW2tyyvrM6O0y3vcOptazkwE653Py/wcONnbCUqLj0sJm+1Orj5OXe4t7LztDK2cy3zLtVWVWes8vDx8Ssua9wc2+m1Pvl8ejd8dKpu8/Q6P3c4OSgpKzM4fRpa2jox6tKTEpBQTu11fPd38O707zG2uz3p49xeYtoboD1qpODh4Pd7fvE6OTQ796txNv1z8Wytr1fYV/p9Pzlz7N6g5e9p1WDe1AqKjzA18C8uruDkaWRmZWOkoz7n4ebsL+vmUm54uuu2/Hb58pQUk2fye/x6+ipvcbdz7vwuKPYuVOij016cU3y4t/J2dy2xtb2wrXJsFZVTzX12NGvzOnLvrlSdJVKbJCAu/Pft6w6X4ZxZkLRrkSOtNre3dDt08pYeZjt0WRBOB7wy1Qk7AtpAABAdElEQVR42ryVSYjTUBiArRWPqRSXzljHBRXJxXVcWgKRolQdcIfBZVR0FC2IRVSCB7U8I1awmMBTgjIDMSWikkMkGXMplATSFikBpQepIN6ci6gIggf/pDOdtrQqY/W7vLYJ773v3zpjPiVyvm6h6jwSfc1wIpIR6+syX5uPKM+fQTWcIeqI+SsPBpuVnDr1HcmqitBbBUGwON8/hKVmNAVQpKjy9BPEMoqlvf2U0wGRERGybUlGCAkIgN8Y379DnNF8baYMKtM9kGGsUg4oOU5FNgQNp1IjKRmVLFNBsmNKJd+/g5vRGlWboig0TRHFygGfTCyZbw0tY+bxSAXJcUUz8ThdKYHIv6NFBNR4MNGnVV6qlgGN3Hsrnx83DTmj5TEkREkp2qCGTem/igCMm5OyOo29WG3ELa3viXwe04aQkB+Fk3JwPFWpZDFQKYm+rvC1YWR1FmE16eUYqPCNE5NjRJzz/RbB1LDw5GaKos5eCz1K5B+FB3FwU1IWjDwGIryvK0x4sOwvRURH0rLXKEAGRJ1nwMIwjLgieHScBN/f+xiDpunHgcChU5FQaFNcsMPXT42F9z288TI8VrFMWfR1EYZhVFg6iqCIJssJo/wySu3F+bzpSAbgRGD+CELWkuROmZEUjjU2je0JBAJ7QoFQKBQOh4PhOjfH5e6OXzFS8jZ8/+l9WxF+UJJlS9AS0ejWUSxpGg02FsZuRuhS1qmgDvt60w6FFmzu7SWALYt71/b2+tNkYMKky3/uHIpLIMJokvKJaSPCyfG4RQuClnJFXmU0La68zWi8K6IkMC5ZPNdeROdhvyoxwfotJ4neGrHhoSAkx9ddWDToivBZx0kybJuMyHHrZjCoj41Q0b2j1zJORTG0RF4UXJEkxhLN/yJKBWKSK1vuEWAySVFlmTaXEXldR8w0RSIgwvE80hyFadfs9GjQJbPm6cbR0cT9kGAIcYHlB6G0Ihg7WeTrxLti3ePelgMD4FGnAI9ba0u1owc9yrz6J7WkQndz9aHFgYiXEdOkpXYibHaTJ/KSpjeOrtlHHzpjGxaIOFaWV+JJh+8UQLVATHGhMSFA1X2hZarUNCZdGO7XM8o+WLbt8kGb+fq1sUe0QWM8UnFFynZLPFAwOJSOxTZffnZr2SSCyNcQOhXCO9Bo4MppoknEO6JQrE7dtrqZPAcqU0ThJmIHHdamlC9LgC8Kpdeb3Rw39GyylK2JUIDdqMIGh2N10unN7kLyPFuj7THvCv1EM/cWN4n0194rDtRVCn6XWPriuRNNOhB3HbXqqBT+tmSCb3hVWfwufQeReDyyd4OVSFjS7NmzQQQINxS+MTQ0BCpNkK9Wdizj6kCjwcDcRd668/i8mgjEIgYt4sFWi/39xb6+HOd5EIR/QmeYvHjx3OHDJ05EXVwdHRp5UoellCUNfFgVkxzJv2LHkRdw1V1v7gwtcEXOUh56vdmvPnhwlSQXpBs8hslrKy91EiFc1m7f/pxwWTRnlreePHDcS0l6+7Zt23uO1W/F5oo9d++SnsD69f7fMFAsFKrvytgT+KwoP7wPH0+cvL4ansLNCI+Zrsjtx80m+o27d6/vJ0ly+fLh4eXkFC+gRUSWQTpSW0TmEcTikdevk7sJov/SeWKCk54IeeZoMpU6trSnp6evb2Wft27cS6b9wL2F/j8hva5WV9lVq7K16lqXnnxGeMxzRX4yYm4xjVRhAK4z05m0Mx3b0C4exrZO7LYwFQnbi9DoLg2UthTYNCW2qS2Vcr9ILcJuE0SgD1LRhSUxiorWTVwTdfGSmHh5k2iM65NJE02Mvuzb+qavPvjPnLYUZKNf6FCYhnO+899oN6NPYKrzwe0rZXJJSUp5/ZJMQHkgScp8993Pb35x9O23P/14quWCSKV0eHgZcmxlpWSPXOP3aB6QE2vUP1Eo5MorIFCyI4VUOipNEi+7Ljsc4h773yJjdx7CImNj71eTa4xrFCFoRWTzfqVKRm/jerSkN6PRLITCkdqXpJkkPDYlKWRzh2Lr8Z++DvbcOTqqhwRPchpE+gUeuFXMl+NbW6u+fE9xthNUSBDJ5XKVAoolyruXbY7++cTufFSSisXsLQeaT/s+I6twHHk2rX+eFvmzVUtoOV7LYRETFplt1ir8oBwwY/GBSDQsSavRok1KXAGJaclWzAQSsVghmQr0fPvaUf3/RqXnttEmni6HHhZ4p9MZninHtlbL4ZX1BGoQKQWKQ7lgxW7x+Vf2E1HJVt51OOzFVRuat2INgmF0Z6psL8HW/75z587dsbG7v/76q6y11EvIYBHj8HAfS6vWCpNY5HdFhHLPB1Mru25LfihWzki+VLPkK0nuYs7ujcViEymg54836/HAx9HGd1G/5EOH4BGIlW9srYYK7tjuspxdo/6eXKynhGKVoaGhSimWiEUi8ahkL19HjkLFbrOlf1FShAARhjjDg5t8QKmPubm5ri64KHXywDZomCiSVDwAM62anOzCIi8qHZsJ7O7Y7T0Wi28oOC9JSUevtCFJaLffHo/txzdSMjePqiLOwTa6zmf5tDPm618tyiJDW7GimwdG/WmIiB3lKglo+5lCYieykoxKgfIUy75Tcdhsvh4lHoT6TBFgG4uMjY1tb8OlJgJA9yZoV8fwcLsZUosksYfu9u8v3vfJ+8xiOjRbsFgssxPJgiQNTZKTbskmTWfsUZ/P+1EqNev1HVVFficH6Qa8Xme+xy3OyyKp5Vxxa1yJyETYRXku9Vf6w3ZkCyY2I2EQyZSfpqhCxWazhV4gYQsEobunyAW5KiCncGrdlCf8o73Yg98DnE6adjprImoGoAoFho6AReRSxJIRoV9N9pK9kmS9hEolmAcQjcW2x7el87d/exE8Wki+JvHIwvBh6AaI7BfK+yCytVwKLXv2ZJEDBw2wB4lgoGBzpNORtc2o5CiCCCr61lcrOCAEA2jJszj31+li/+tcMIiQGM7d2FtEa2swLC4vqAiaMYGIxmhkqtC09tRYEoTZeHxGkKnd4m7DpS7SOR7PhwpO52E6DYHceje+vBz4Cnk8Tw+E/asiLdMRyqfXbbb+l4ozM9AOd25RVGf/RHnT0lvNLID7v+33Q693HyFfdsOLUG5tZs097VNp4A+oTaxer1fVTHTkSayCMCEoaCiuJqK8yETLaJogtuMuZ42V119fBiIej+fKs5Ktk5bpkgCy63wzpBMabLZ1UhTlgRFpwRHR4XXPQvr8ocaBCHzwVXAoidB8uD+BEIpls9kZpDLA/jV6AIucEWG1IHwTFQCKJ+tolVVpGX07iADOOrmcp85Qm6xqbOMVOI5vAqgqRqNRRQJKQOizA8K1nPsbTw9ovfjJo/5AYBOhELInUOBgdSOb3XGoDCxjVjwMNQ81eQIeFGauC4Kr7kczOkKnk21ooKm93Yg16oiieHGqZnIFbhqNJh7Q6lo4FXgYjkU0HE5RWJb/l4Jyj9bMpR46wbsfxAMBH0Jp+SuYDmezhXRJZVQZsIcGa5weS5xLEDoe6RCMfP03NV2u6tHO0tcaPSLi6jS6Ne7BdMOLKKpF9qBht0YQ0dREzGb+nmOd0+KTMxp7WyEkx/zV6t5JxksIFRCCmt88mC6g9bSKVZmNZrOGZWqcPJgWs4BpaI06BiBwhRjBQ86s4q7XuxiJRBYXFy+JKX8WWW5hD+eTtIwsYmIgi7FHFY2JvBfQgDjlu95Azn3ZKPLlUwhl7OgU9cI4u0D4mgdNHsNQOG5q6KrtgJGm0hmxjl0MgAiy4JBcBhETwcvQ4GGoeZgYuDBnVwVUEvYAGL2BM7X+fOzxc+s1k6nNyjoRiljHB65arWwEzWRVjI5phDhVH8AhPKz4R0LLQ8gNZkbNKaVOtcuwNOvDDpagKGY2xLAs4t8N+RdwavEYhjEfx4PjdBR1vBpX3Tco1MA3zHoXwUwuHb9DfHBSrVazVqvViTquDgwMXLx4sdtud6vU6gYT+qRHG5THZ9fBw4VXITBmCvuaqCZFhAYq3nUISqxHFHe8SmqhUVswGZc8nvG6iOJhpABGS2jlXsXVTwzHmOO0dar3DHozbGzsbk3k7gXwoKwyAwNTHVZWZqq7G4ucWectLCjcuK6Hq6kaEEwtAXmD4mGQpwTrzef3xZ28KPoTYkYRGc0mD0YhJE8+aVU8TEyTEhANdjnZ4pXqBI3TIpxeL0/qwdab2ONm6zXsoXm2e0pg6zSp1JTBCA1L9a++OygAsVctTkFT7Yxa7CEvChkGE7ldQUN3Xbiw3XKY94n7IVGcDYluEAnP+zeSSXuLx6PTTb01tWfS6moimn+L4HOsOeg08JTkFKChMsD5JfwpyoXzTBtr7ZgaGWEb0fepmjuhWoxGA3i3nKg40OhYfub1D1ufGiQIHs9foDqEdYyJVGMR1jT3y4fPkUI+KfZDrRwkRDuI7OwvL80kA3AgOl2ztNI9wDD69oZZSOM3eMciGorGGgxrMBi1eNh2DIyMjFxxUa6O0WVZZGu5u9s1/mz3wLiVaWr06FPFIUwuA8BUz6ir2fawnFiCMPXYp698/8bzrz5HEFDlPFGFUYD8ZrAITc59/JabHvbOiOIuEie8on1j2jH99hsX34uL8m5bHu0+N36FMQ/3VUUWuq8KcpShsPm6CIUjQlAGgMZDir26IJ+nzN7SHzAKn1l4vGlhZGBco9GYh9vredXXt/gPneYf00YVB/Bn27t6vbZpbfuwNAepdu3VbVdpJ66EbKyDMZAxcWSudMhvBAbyawMzQdmIgq6TzWAcmc4fmSFOt0yifxgYGVmyqVFCNDPOaFzM/BGNGv/SaIx+33u9a4vzk0BpKfQ+9/313rXo8LH8fLcXLgokCD7KPp70pFC5Eu9JJM7ElPM+RQ4/o7N2yXBjqYKbpo1bm8JNivKQ4+zpOxR87uNXWlvfw1geGHhlW3lCHlB6HjqD8RvwZ1gZDPfEcbjKCBsHKtI3FGmMRsGBNqiM1OJoPCTwQKlp21gZIq0GRVpKS0PK668rERvgFAiW0mpJi8eRbah+LM/tL6x7uCHx/Gid+9HCkgqfb4pcbGntL78t/sCT7ynTym1vjseWm9rCp5KxuUSO8nkyORPu6pl/e2RkW+KVl8Zjc4vTibsCg9Fr1+MXesIP3dtx4VpPl9L8Aj52aj62HD4Tiyff3CBWV/dBnhi6G8mp3pTdm4yCWiMkIBJ51MLzLRAN8MhHaD1sZ3fK4UeIhyQIogA4qp3UQypdH9nQimo63BVTox2HOhP1dTAg/Tv2gQivt+45cbpoYGbkpIKbBnuSCfzEPC4bCeNjy/jUdUW+Mo/3x8/jvTE8kHxC6YlNd23Vd+0euTt87Tp2D9x5/FjsanhmGl8b75Hnj+GeuKI8D/2+trYv0sITWlIiao0IdlVEABHWE/lG8Cj1eESEkOgUEKoKVadEdEyERWR9qSA+NobGnnX39peXl7fev28URJ6oeRlSS6/vOHzixJ4NAyfPxa8MKOtmEon9cWXmif33yNN4vGzzPUpc2R/HHrg5kxzYGT433aUHkbH3n1HiidorG3Hb9TlMRHZ3De64Eh6Iy0FE4VM0bsoKiJmu9PINJrOJJRaMLFelDTxKPQJSkaodgpOYWEykRkqrBVYgTtHVDCI17oZCEHn67qltcO2sd9dhklqPjh2uGevoaUok7l9exDPJC8DA8vm9xYEwju9f65CTYIYdSjw8fSGs10Ru1+Pk/nVXuvDgtRkmMjg4PUNEqlQRF3RuV0ZESEysHCl2u0MwmkDECXZGm80zZAPAQ8MG1Q0rdWjfkFr20upqkQCJZjB3j6GGTnceRKSzNVHS4M7bV74DRPZ1VFQc3sa1TZwKX32+J55Y9zkOY4yvvD3RKE+QiETAICUyMSJvVVIid3ZtxfHE+WUlnHN+U7ZIkHlYWQAM+WqN6JCLavBk6pNbFhA48Q4bwYk0RJtdFOw2Z6pAyEUgBoiYH0R+qIy9JYVFHydO57ndY/v27PO9U1LI51YUtbYkYs/Lt9XG5AdiD+CedfiFkQHl1DLedE6RP1/GKRE8XovfiTGRxYR8/rqsJMuUt25TqMgpItIE+YYLqEiQLZ/hx5AqEjQCBht4kPQSaIWYbd02ij0jIALctZGA2MCyGnDwOpeFt5gefhtWmchN2bAfem/O0c30zscVFUHTmhzR1HVmPBlbnGhqKhsfXzzTFI6OJJcVn7IumZwLd6VEBh9YjM/PlVGR+QvJcRih+y+MLG9qakqLhOdib7U57RICDChFIyKc3B0hIk7wcFIjex/pCi0eWtYoi1SISJlAXsHuwcw4G4JviAzs3FxydQW+QsXAnUvdFWvp2bK0ybIiD/L8oIzlwYjYhWVljSM/gbHS5pJxwpHATeJd8DuM22hqYRx210ZfGoDfV3Vh3ITxM8/I+K4CRZaPaodjR4z27qH2Pk97O0zq2uHi9Q5kJOz2eBwegERDQGmcxAAQ6EYQvG3InIaJqPBE49XmpaWlI2zGIkESq3gjgPLp4o2vdRBsESPBAYQgRbfqAVYjVpfLNbT7OHy3kn09hSM5ZbBwxpSIE5G9HBwkrMERIIWG2iuj0WjlUCS/pa/RQ6E5lFEgEsRTIHLkO9xKgsW8SsSkeuQTj8cfPru0FEmlL+npRoJA/pacsaiDItF0CF1c+fbyF1988S4TGZx4oUtfBQ59IfgGvYhYSHaa3hLS6UDIYDEiOKcReix2+CFdyZ6W7uHayso+MslpGQjImVEhEB2LucpqhvICCTgUU7ZHLsplFiytOg4tARLiiAhHRJxUhASVlKLkKKYigvHgxW9nL1+6dGnh4sKli0xk6yBMxdyjrmBlJQ0IBx6CQ6J5YbfAGg0ZdQy70yk5WSFnM9wNk5z8hR3qg4poWFjDs9IE5HTmVegQBMRkSqXVsepq8HAgZKSLaSIiURERTl+qJIeH4dvsZeAi/Zccx3+xkrmLcTVWtoAIDYgmIlhJQHQpEGDUyl5DiHgINJMkFgWCAVoAaDDMyHwz4GDBA36gaXU1VFx9tTpE/UEEIYkdP2Ax0nhQfvttgaNYLAWDTWEl/M2vbarGBx9WuTa5CLk6IoIcAqKQgCBOE2Gx4VAWHgbKxsJWAGmRD74yr8bKWRD1WEs8zoZCpdXFISoCW3lESwSxHTA463iBiQz99hv1cLXJWEUehKyy6tbCWyauIRfFwkQkNgl1OgNJc7ahUZUsiMEjihOuDUhQAAxj6sZEUXfzOsNTtzx3Ew8Qgat/QdquQkBpKKSeEfAArDp4ktXMOAhEPI7Z89HabtgWYvn03HgsHhufOw0t+Bk4bv6WW34Ej3TPEhxQtICRC0KZkTSxQL2rSWZclVqezAVJKl46Uxbw4Icfmhn02Dh4AtwcR3CQIvF4uxokCIKR7gIhrwjEQYM/SJjlXOJQ9ICMT46P/PDJd7d+98kPI+OfYXwXFemlGoCZiNBBx8YyIGjpwkRWV4koOjOfQoNiygb2XyBHLufAVNco7itFZquHeVQXk0HkFNVM5qgJUuWJUZB4rNAKOYCV+eT3t6p8n5xT8CCI/ETKAzRYRETW40jaZIio3asK/QfBTjsarAJg+Uvy28BleYCckVhmMzwMW90I1VgHS5cQYrh0DNFOmJ2dXbn4NQjwOisNyMJB8CiQlQuLr92a5rXFCwrOiUTbXS5amKz72omHkwUEEKBUOICeJwP5lfQfE6edgli7NjDvdEBIzYKghqG7MtpeCiDisa75KnisLXDnswbDIBG5sfLuwuzspUuzC+BC4Wmdh/H84q3ZLM5hubcvlVdsHErMQw0Ir3fB5W+tPNgiAZIpC9FOdrI87W2c2tf4VFwspMrgbkYwartLKci9s7jYD+/lewOBwIai0jzC0aNrAu68R+z2hZV3c3PfPfouTL3Ll79duXhwYYUjtOGTSTUeWkySn+H9a8FDCwjtcpK63pM4osdxNOA8eYzGy74qKNrUzKXO2qMWcl1O/VmFHxIEqrHeg+oKlw7V+PPK9zwYCDz2cj9oNL/4cCCvueThGzcugwYFbL4+eBGSjGPI8rhWHxrfj8u4oEqvBcTA4kGxO0UD82CZRYoZASJaDYQhDSfCcP/v5FQ9UCgqCTbYREKDQg39h0brR/Py9vaeCAT27PD7t4zuqvdPlpy/cePP3DTQ6tJsxKdH1MP/+2/NZOQ0blMDomMFos5DuDHRuU/y0umETAMFVrg3DwhgYQMAFg//fRKrD9TeDmloqBLAHvlLdvi3nMhraD50OI+IjPafGN1+3t9axzzYFGIWGoN47gd27P8szJyb+zkl8sMcbtJrIgL10HKDSz1OMoRWDEkWC1qFgWeRSI19GI8WLb9EuorJRy3DjSKteSRUSiBi0ZEgo+k9Zf7O5h39veUv7/CCyK7+zi2f+f3+XUVj4MEMVtOExz9hx74ueeWza8kv2Z1PxrGsilhTI0Qd30azJgLAERo4wPg/4TAYwTF9VxBSyy+48BjdVNlYOYQolSHwEJk62lu4AUSaS8p3vewN1Pc+1Nv/218b/MDki7m5XBo4gRphHPuO5dUIDEI8HWMi38Uw1quYbFREHeDqO1MQBY6xOhy8Bb6yJr7W4Ug7RtAVPLW1x6v4oMnTvhuINgoERHu5gPyTfv/o6LYtra1bAoFnt7f2dJbUN/v9nSVFR7LiIRg5DRnHUzEYwUq+jMdTIYljvJUd8VaTQFsSsrIk4dSM421OTiW7iMkTTZkeOoZJJzARQagcFk0ml0tnMPASZB0DdDni40/R4If+6+Z//fWM/z4+N3ftcFZegbIhKyKs+f4cG4CIDMT+YA2YRMRVZbXC7oporHfY2L4NIXMq4TjJIXEqLuNNupVLDZRRE0FOkYNzI/Q1umjFGqGSzFakiXA0MJwoMR4JePOlbxeCwWBVLoPLQBQQywfOZDWla+SX5QE8sPyLViPY6qLAKCRvLtrpTppDOnWvAifExaWwmiyrQ8LCp622VGiUedtuNhQNBo5upQQGdF4qIoqVqolbWlk4GASIRXZALAJJLbgQiOimFs9/mpqCv8QWY7+kZuOn8zjsYqynQI3oIST0Or9W6ZoHLCtvWunGVSZsMyLamAhPwpEpgqiHCCLt7R6JAeEAWEj0XCZGKgL/EXFQsBth4asOj5+/hLxijJ/Eg8xDdFARJ7KQ97m0CgnynIYOfvM/o9CS/QBLSxsTEUk4NBFJlbFwiBcjw8PExHPjRvB/RcCavjZ9DVGQFda2NFjTUvBG6sF7KCKkC1nOQ4UwQse5NHrz/zRfS9qMbTjMqojHBMOULcahEsn6kmnQWcOJYqS9HTwWaFppIgVcdomIVISsjCD/H8LzMBFX8cM8Hti9OyKK+cwDgk5zKv0Zk+ORzNTKGtgWLZcy9Dh6hycBNRIRGzQwZ5BUu8FAV8mqBoB0InBSgvK4SBwKcgpYjWz0ZapAZtEb6ENk0D2C34z9Z9EYexMPwuIfFDQPa9b7a6aQyOmq0lHOxGpK5ZILpVG3XsQDqK20A5KBQC/wkNKFiKVEDCDi90+XHdh83z3wKZInAkFXLmGXj74me2ESkCM5iKydbA4gjJeh3LP49ALG7NpSkHnwLn02kRBf4PNtvPlANJO0NXDpbSOpSDOpdeh2yKmJwPWplIiRDBAWDtoZedhg+l8oLCws9xLu8W2kAcnx+XKgy9T5Cja2beRAnNtzBB0bg8HkkATpEfxG8rVVAXmeBETnaq6vB49HfD5o/WvWrNm65jg4HF1zVB8MhbhQSVErvGpBTu7qkJggYjS7VD9O89CxjVkoWgl7FTgBBoJT0GVihNSS7Ladf54sbPZ639ow6V3r863NF4M+QuCtJXj/qnzXrokJ/2Tv1AFUfTZ1tcmewDO/Z4n8fp0F5GrRjrydOw889vGoz+cm7J1q0evhf1jJm5p1x7ftDPga7tu8mYkIqEAtErqAIT8YVjVfHQI8tuLaStZADRSBy/QwSMhgAzx/3Sgs83qLinZ5S3yBwtbWOp9v0uc7VP7wVMBX9mjH6ETnZN1UhQGp2HbigRF11ctm/BkICBCp9xJ2PNa8zVtUAh902zD1Vq6+cbSjo6A6GNoCHx7a3PBgs//AnZxY53y281AOYrCPtJmhkMnWloPUZ7AMFG2OaLSY5KNEi4SHgZ8BJ9nJtW3Y9HzhL7wv6N3+qBdE8op2+Up8vkKf7+neZ6cO+HyPjtW8UjdZPrWDT4s47qKzRGP8CpZphdxTVt9JTPbWdJRt73UXur1TV+EIR2uaC0KG7k4QKSh78FFvRT0X6p/4uPOJAq3c05+pSR+kRd3J21qiteDBQ5VYTC5yKSTDQyyFxRhsmg0HV/7aUOjn87b7ISpef5E/0B/wbff56nofm8prqBt9r6amY3Jy3zY+o99bFXzu03Sljwzgj3SM/OiD3orOp7fUTG/3kg+H7SvWny0aHQMRcYmIbN724IE1FUXc1X7/syeOiBlFwgANFY5JEIqjm2AAmCycVbuqpSKsJyL/0nFuMY1UcRhv6jBYRhAEZqzYacZ2aIFa7AVsyYYCsa2lYGWXwC53gQVUKAhyyV5wNz5ARZBNNLgRiRgT42WNq7smxkvWbKIxRmM0GjXeL/HBF30w+mT8zjkz0+Lil90yLbSc3/zP/1y/g4BAffxxnlw/yCvbSh2pWv0u52FQOJ2j8B47o/3Wjo6N/nj8sDXf4ICOilMPf6JXrCOPiT8+a9Y0si3HGgY6OvrgOOyBY++2q04fXuk4tV4b4G+vmQm4ghlXXWX9ztJ9D9SvVdYYba0+k9Fyo4Ch8CaLBmIiYyRDRkhMpZizl5SZCMfPVXmu1yt5JRHhlQ04QWmCHHI6N7a74/BKdw+fms90Z05GtN+o6S7x8xNPa+sOqFiXNJDm7Q7UrOHM+MZKM7W4bTRPP9DcPYyIVPC1/fXNTmd7iE939vT3bHdnYkkTtxekgHHkRgZzSgICjitBCm8jsuUB5FIetO5SeCVSx1f5VJdTkwtXOQpxewelN7jFV75nY+C33KJ0kYHc4KpUmOQVGCogPFL5as7wbaoT8sECma7UxJv2ASnQL4xsbg1z+so7IlN0TbmlhIkMTi0IDKctrQc6x3hd6OH1Kw7i2TvIONOGUbOhh0Tv25/SBJkS5y4ChClCICrTqTNdqRQw0qlUOmJNp0p8Ph/5xHLts4XiwH86EmZO0UGuMSAsnNk8FO4EA+aK+vJRWUlW4DCbPmaDax6zL/5Kab0oFYaaeHduY+EUn2/5+uqvW7bEuy5evPhuFkQ+AyUaUykFBFQlJYFgHCCGOLqXQ00N6OZYq5WNAZ4YyYwBvGkoPFKBMZYFPyvgfgoA2cuRZ9KmCQtD+3HYLOAQSnIkZDm44oq7xGdavm75XPRfhC6zl5OKklDkpKzExlIpeQcQrfhvr5gPBoONg+o6PrbOV8fbUDwiOnYjl2iGsirIeZZfWzsTXgC2LgsBN0rE0TV3jg4NwMHnyoI1WXLTyqhDl7GXsVVZPhsPFCDgFSdavhDddnAc+0qLR3R1FA7NeCwFJUeaU6n6ldi5xSAkW6PHj6PiLZ086SwGCEfWrkqJ2Nxjfw2RjVLBcuVqN0b5aVmWsW6JUuIDwGFv1BDyiooK2YYOPLSa4U2PhQC7MX61tsbN02/XiJC7roy/ePE7CtImj8fH7w0qE93dO4QkkTk3/NiFu+tHN4KV91mt9cebNxRldWniAWwTY4qUZ2LrpjnrcmeGarMQlkBJa+3QTGtFFsFIEVve+TVZnqghbTTHm9h0o5FjNQuuxiIewTZkQUCYUMdAjqaQBcUu8DcArWZq9q4aYlIEyEUzFG+ObzzS3tcwcKEhRbR1ew8Xrx+M1Qfl2wGy2tGfUJT4qYlKsmNRpAXEYoAMNc7MwDIwCTVCkyUHKnSV5Wxk2UgdMc/0y8Fxq6uydiSdxGYoSfUwb+GZaHNmqIzTQWyY8xdx5PdVFaIO30AE850t3FnMUV366hgFuTt+/JHYdMPo6LizEdq6u52b3t7a6qlUAHI69kLHo1sTjz0wceEhcOSb9I0HBsJPalbzA1hlx1h2frGxwpCNzVjKgWKx2cqQroHt6QksvZ+ur59IYh5AQBb4Yo4kdzGbbBcX04CzXMd7UKeggqsQESLtzGcB3LcV4ckK2mVxx776zgxNZ+KHbpf7GhKK0xXGm7ZGAdKEMxKygo5+NDb73MGDz08dFO/3P3TNuoUFvwzniugZnclWBmKjWVnnnZorbqUQKZcUEEyMN88YKJUn+rH0fq4+pmSmsfUGkEbkBVw3pPg3o/wCt68AwptZNCC087CtjoRL7WYI7fSzx8yTk+bKiUwGCX2y+/Twi514kzTezsnHt7tPKgTk+Pa1J97+/q233hM/ilZzRuBr3GE33KtCK0AYSRkKcTT2vb9kkYLcs3V/tdEbm/h8dlmUGP3MV1Rx94CzeRr7UwQEDHpOMPHlhVeA5AFE50CTT0FmJovph+On3z1mHjlgXpc1Y7msNM7gTT6pjkvLg0G8VE9OU7ofvHXW7/djQ/ger9/rjlR73Xepfj9cj4IlgIRzu++p4nzkS9u66L7L759yuwcG3Gd33JI+PDLlA9pkwe6PMoq7WNEzoDRV0mRfqC39L0fRNTx3heiyt7bLQEGQIrU2dskJl2mOaIBEjRWcoaKI0m6NwFnjPnF/5cspzv/e8+IbXz78hbjzYMt73hhcj1N1lpGpl3YffjDqD3m/aHm4y+/cek9cObvcciImPnyk5cu7dBAaDWF3w2yuHsfT9ZPbmaiTgFSEgcBCYuKY8nGL9wHJViytwAfCpYIWkGMGCG9nGuEMoVlXFF9BIQUhLbb3paj40lsrsysn/O6zw2Jvi+gWKsYOHuny97X0il++4p59a9nd/pL4/JF28eUW0fvKjlitNQmFFKh86RwGSSSzilzzVpeLgCyG7dLAgDNdemAoYecMYUHlf0EibbRPtM+Eh0ws17nvLn/HOMoJRGChcWgPRxHOkDCQI9CUSEDa56TnH75f9Pb6e1vcgmXyRfhq1/2vNMKo5lO3HhYJyAl33ewRf+/ZHXAwEI4miCGdjTjNF+3p8f6GWGwRR9t0FJ4/Gh/USCSChPdGNJAizPsikYjZYl/EBJRwQJcAQkYW5QLhGMvByC+CUPmqsCurRcTrpSD3t3ofe+vEsru6t8VrsXe1AaRk4AtH7xF3mYAHCuKt8R/x+xkIByO/CQnyH5B8EwUR7PBJNR2KxmJ8+6FocCVw22d1VrTF0vZgkoPScvuhQ7Is2CoqfXhXKJVuP2yFBq2oPRg+mL959X0KcgOZt9mJahfHhCwIT3zcPICIcwEgPlPY4qcgVqvXK86efc9NQAIUpLT3bJf7yGzN+v0t4hUg0JWDGXARFebZiW6Pt0cH+ZOwlq7cfvvhDRfG2pH+wcEvuAM4JXpoPBOtbGho2lDwPt/t0fZ7rdaJu7ubF+z2ySV6lhok3z1LzRE0OVrH9Cpq29N+QxSktbOXgDw6N9f4htu9/Iobt99b0wiQ51HLDopnh93us1/kgPS+tyzO7UtiLN/bURMEIRjsaIohCtFTwyuH464mF59fpTQNJvq52xowLjve/fh8Q1zul/HGd7qjq48E+xr62hvmUeiffqCnru3HvroEjgK8MtbYumDEo4TFBDvJeASHmYLYF6e+f0F8Izrn85498eDbUyvej1q2QotjU0fOPtiy6z/qfeXEw+/5qw2Q2bnBljf8JiqMDwv3pgcTwTCZgtAj/FpPpnl85bBsvc/K56+7mtaiDdxSg5zIdHefHmyoNEBijwTJGGSCgiAi0LeXvrqIQzp4gTFkQfJwLol63MqLiCfmHpFaZ+dgs4cRh1pz3AORatjpa4QAqpYXXtuQ5He7e2t8sNbDOSn4xdk62r+YqkKqROXTZ6mqqobaGAgwKMjWvXy0Z+DU+EqD1XUYVSvkbIrON+xMH8ZKQqz7brlelrspyPZJnJ3ra4jNz6dQ7vhrwIAw+OUteN6aQ2GBozLrRSrMI8muKcTKg0qwrkSIhM4FSx1AKpMR8o0abJeGtEKjqrIrHhianFKogCTsUTWkqsanAqe/uynKx3pG735g5W6Xq93F86FEU1TeOLzdLsvNpzva5ZgstxOQ9ETm9HEy2Di9SfP6qQ9p5frzkh2qmFnIAYGXj7uKUdCWxZzdo/JJY7svSipAkpFY14uRpGXEZuFnv3Q7HI7NcFqSMMlelxKbyzOSWmOPbSU8iwDxSWrnpsezuXn+Razoolc+Wh3eXFPN8M3rn6sE+DSfliutLiaATPQQa4e8RyTZ2VqCjLN6dog3Dz0Fjt//HCEc2dZKoBHB3OmaArSZxh6UflUn7XqWFQISiex6dgFiSVvOtT/Z6aAijADZxWUCIRkcTDg8zmp+XWp1aNqRnVJBUXW1x7OjFqH6EpMU8xPzWXXxUFtsNTafTKcCFcQAHWD2MdZzmyAbnNB2i4BtW+FDgPz25xmADGWzHANmJtMe5bPnlioJN9YpUZBlz2YkApBzCMZQUoq8uOlwrEk+VK1llPg8QpICiKOagJx3ODrX5ke6HI7lhNNXdNTl8ZwPFUFkimKC0sFgZVoHSSzgwb66ujpdrKuMNdWQiQknOuwW4tQzHyAgf/wZsC+McVkJuRy2KyaqPgIywCLiAYgyeI9/1j3rv2sOCZBcduxKapsKEBqSSE0EIIhIFV7ZVImG8I2kWqAmNZAbTCUUpCY40fR6MFmXXRHiE6trkQj6u8krQATSd5ciIAJtCD/84QekyJ+XnjA6D8GGK02Ug5pf8T8rCSC7AxJ+UYSUpVfMak6Vxkjx130UBCGRVALichogSPAd1D9creFjAFKIBUl2t+qDShCHCKOJVsx5hXPneH519dBahDAsUhITEQWx2JnAQWzJFdM//PDD53/8ecmgKAE3lyXBS2W0G+P+A7LsGpB8dZE1z8vevfbIuYjDsST5KIiHMEkERJEA4nF4VDUPC30xhyPMQDZDRYWGg+hcRrlBycCMMtrULPDD9aOZvr6Tx8fHzwChq7S2tNjG6bYpE/YTEROIlE7AeuVTH/z++edGV15MxeVKG+7l/wfEE3YNDESSS4+K1B759d9P6/bI3k3HshRaJyBrHhISKQEAAoIIRUiFVJPIdwNEM6pCmxnFp2SOy/dWxpri8fqYtRt/FqH91KlkcfHN5KRGSZXh+qKPBIRWmtsIyFNPfa6nh4Vi2K3XX6jLRkQzaLD2IgsCbS57PI/uY4/cIhlAI+IaoyF5sXlcygXxqWgZtKql3mDiqrIRURCRtXvlgZ54HH+ABRFpzjSfOlBe/tkdkAU+XmZtZ6IVC6plIH98bjRWaOCSF64nihi5zgJCwlmOL1QqQJheFvezR4YdaxqIa5M0XLHmiT0g66pHBzmv+qryAcKEs6lyvRK9N+ZsGmzPXLjQ0NdX7zx16kx5BeEgrn2ebFXqEQQGVRk4OqcrBKP3KC6Wr9d07UCVDoIEKTTrpuVckN3zXd597ZGznoQUYiCLDsdIJNF8HiBtBMQFECTJsmOZgaCG5QMEH/zOjiUS327+wpRcu7cHqwQYI3YfqhwcPT0+IdOAlPIAoZMAvWDg0NQ6JhD9rKEUKzoG+euGqn6jDPO1/kKIgHRiNNL7P/bIR+cllYEgG3bXEh27Okg1AalSkfY6SIiC5JvOjd/G7nSN0jOvQPI8vlDBqTJZK5RTDqNfztXQmF2Ajj17jHK4rtPjcSOTFhB9TzCvDLJZGMiySwqVi/9jj3RjXMVApE60tDhZDpB1A2Q9iSu9aqk8QOi9DZTMjJig5CAo0mVEAbtArB/CmTBzZdBeOadfY/atLoHp48sC49BBrmMc17loDLNzORvZsiRjIoCQe6zm2CM/+fXvHHvkHANJohfZdTQmmpsVAuIBmlrFl6svklYrRFstlSSJiTifsT59oNUE8VhKT5Ux2TgKIizMjNir7AETpK+I652jgCGJgFU/QfjlXcZhgNyogVx/oYZERJ/LMUefBrIsSdw9sEfuYzT6C/ZIBiJBKLRneLgugkEjRi+K1iF60mq5pKLZW0NkJFNFCVR7YHFGn7aUaRIKCQg0s7iIDh5L+UJxTkRg9OHHZiwC3o1+5VmAVF1AZuytWQTrYB2oc/M8F8Tk39/69fWDopeCeDCMR2lRj5Z5HiA7pNWC0AK0SqFC9WjY41mKYGvNhOOzrfi3oN9sm8YB04YGElhICTOd5OxW8d6qFZiZFATCgYhc/q7qJgJw5y13ZkFYeG6KICIGR06y7wLEDXukYcY7aJjxYI90txEQwBapbMwrYEQuDSE4O52du6RiSW2FbUcjy8iS1tiaaWxocnKoNtsKafUKpSunIEyTY+RQlHaayKIn8CQiAs8X9OblNw8yjlvAAITrdA5E6bpriwyOLMimZwcgomGPfFv0Lh4U387aIwtVaRddCAbramQTIGcAQvKFqVNFQAoRkjWQQKb/ChA2Vri8LIh9DCBUFnCU6SHhFxYEtMxVnHDp8uWpa1GvDt55LeW49lo9W64jz1RzHnx05Tk0qpRYWpNUEpGndZ+n6BXFfxjI04hIfgg/k5DUgoKQlFxa3k3yZIaY3CHVjEy9QnmFkHQU37sCxPdvX+cW00gVh/EG22nasYwMHaEiQ2ygFYnS2trWqqXVLLILuLRFWiu3cLEgF0GXhUiwsEmjaMRLbMC4jRtDslY0wcQHML55iQ9sfDIxxqdNjFlDom8mmhi/c87cWNFPW6alCefX//nPuX3nrJcMrEZ5KtRsTgFpUECc1ICrgWAFtVWCLPyLX3x5AyVGIBgHxIKhvIqYlYRXhzVs0Oo35MijJfR8/3zqA9Ueeb+1lX4mYkdI6BV3kQ5+519bIm9LDipLy+RkXXd3HR0rmDkr+5rOrCZ6vd6VNiZe0/jZkfO3nQ+00UYdA3EqgDwzLlHxPV/8fEwQNA6ACILhlboCYiYY0B20jCbTs5o98pdHfyj98Ogfmj3yWbRypOB3oGK8TkEC57jXIwzudWCo6onUTU5OYmDl1LY3jMx4PWmvJoloGAoOYnIi+pwMYej7GeT1m+Y9GItJUqQZH3ClIFZqhIXUKw2KcdxZp4AoHmViuqI91jrd0vLT34/+/Ytuj5y0kE1qaDJgLLVSmceVJXOnA7I6dEnEU4sFTnX4OpjtJQBn8chkMr2S9IkkTXUOb+WwAjjVtTgoy9Oh9cHO0OpGqNDpObMxMca3bmdD+UIqVZg7KBzcKeYv76LYwFAlMD30UBUTyRNUWf0u/p/2yMeIVYiIYTAUDjKjSmBvik3nsJB5JhyERf+ACXptemIQEDkv1i17sxlJeleSpnfuGRx8LhoMZlfhz47tdnX2hnbWEtOekTeupLO9r7y/XU6OpXzZuUoiH36jEs4e6CCiuw9y4w0NRO2u8fQJuvAf9sj7ySlIYDip2IMXuUbSxlnx0FRF5pm6htLaxCP/2shQzuudcaXJumVOkmYkKVHoHUxjnXYp2xt8/NIqtmgEQ2vpdMgzghddh/n3t7eTyVR5Zi6cSOxcyh9OIyQERah3A0MBQUTuMcwM8rra3/kPe+Qk6wtAKCWRGTahBqx8nCONnMMWsBoCQjqBj3s8PQoINDwcWvMuutI4vSqzIUlvcAApRHem3g8OZh8Prl5KJNPNIwoISp3uPLzkbc4nUuE35uaSielLycOSwNTHlHLX06qlhcRKQCjL7INv8YH+2H2n2iPvroJU2y8lYScoQQ+TZi5gqFlk7ydAzizNsxyBamRPKO+97PVOuJ6LYt9X1/dPRAGytLIYDA49vjTYtQ1LTDCUWE1veDyXgjIU/axzMxFOzRxcCyXKl8qCKhYOQc0RLSQmRXwghhN/YrGBydPskTQgFuZKgZD3yuZnRgKZDAGhUUhv7nSTmku9ipfj8S3ZS25cyXhXWpJeisczvZ6NmehaMLgy0RtcyoVizZ5QaGJoENsColtnZHltKHQ54U7lstmNoruYHRqbm9vdU0MiaCCQn66uaCALMTa/bb//FHvk3eTAQ04zYldZsamT0JicKonVEBCU3mGSd6Y8EnIdg+1Gv5fqLvJ0pmuVNBDzdYjBSFDXZ80vh95sIpOdMpPHl0wKgg+5QFSsHB0dAgRCpVJ1JwG5hxYKHRymWD8FcdhfPcUeOQkOs1WrWxa6ed2iBQUkxlsWqO2m3IRnnqRgDe1/OFD4Hn+HS57IrXslRT08P+BUFKhu+SzUznxKNfTHA0IJeZEqulUdCSckoil5hIA8TV2LJFUZCFtiRDme/Zc98n64MsBYxYSJY+b/YzcxJH3trJM44+mdy85kGul+gNpN2SwcJzG1zs4+4FeuLbwGAlVXtzRJlMPZSEE+Y19/KayCXNdI0NMiHGglKcmrCgiZqmsaXTCrINV332yPfNVCf6UtSTk0zxljCcScVACpAgStdMpsLnNAAgR9u5tkxuBqWeXgq4k4CFPtHOQXmCp9blXHNxiGz5ei8aAtPQkJA4Ga2hfUNUZSkJvtkc9a2a80EPuJwwSqb61VQKwWgmGiCrQ39XAY/DEQMulzEwdHxlYNKkgVBbFzxC5m1gOCnlXZl1Lr1s+/zZG3+vrqGYj4yJ00S14ASK0Tal9e4JWFOTvRTfbIVvNNIGRhwohyboG3EQ67tifPbhofGCA8tUSICObTDBB27IYBxzMMAoNaazVTE1w2lKONQtC0LqbLLCqpo+Pj31hUGAgomIZvwbFk0OwoOAwgJ+2REXWtX5GDbqc3RMRyjnTQMalgkKnxGYDY2AYctsvRAIJvkOwBiFEMevCCIkxNMItaABAay96eW9GN345/PtJAxIdUTTqJRnVLBGcn4lqM9ki26qhHhATEYsGDRQUBGD3XRMYZJ0HGAdKogJgghwEEdz5yOg9AGnkTpGDYW2m2NzWix0kpmABCaxeokPJ/HRtItJjQqQ2jl8AOIbVf0O2RnJndsCyanZWUXo8I2bq2PI5ymVnFUkFGtYiwKXfJwjEKiwTrNmpWw0JMWd/wDw/X3YFUp2qsoRIMOiimUuBA0d1E3yggkEbiJSD9A0YQC0cicD+1R+7efj+WTKVW1GoVg4VBr1qPWaD2WYBw9DtwqCALJEcYh5PDJR5WmuPmVgnsfGABFdpEtbU+veSK+DFh2cMFAOEMnO8QjFJBxHr6/E0lhR8KCQScOwnIuAEEJebMUOvdt+cf3cJC7y2Q10XU4ScgsuynmaGlCAGRBgCCWKDHo4HAquaspRWrxqROJSIYTYG2BiKMctWJ3k9zWwUPtTwokl2uFNpCt8rhK4cVELdbIM+VSiXMMDScCwGQLBjNU4gIUYTaI9l5Hi2uqczEWfyF12GS9MhYKZm89VZ/h+y/5bEIBsgAOcvzhIBMxaogMXIUz71si7xFIUFo31M126hw1C66qPKhsaXm0FBoHfPFrnrhIDSN7CjiUd6ZCF1xu31dE4VCSlwVxUKxshvdLAsUgf4nCinZCb2HxUajkMrVr95/t3qyfcS1ks27XEu9rki3nIb1YiXd3UK6Q5h27MVWVk/v1hmvHUJxSUPiYCBMjcx0Q/ahIL/Z1LRZG8vhHsHHXR3dHXJiJZ9JPL14djCeW5vO0wpVEEq4mC6Xy3szB+6JYjGZDIszonh4rVLZvjLT97b4ragIQ2K0h21cP8t342GHRrk80aDf1TsWRw87KcuvrGx2vhYdCmXqWtbXdnD6+Gp+eidCa5aV0CACyBHDQVYcdbxih98sxQCXlVLQ/9u7XPNtbRezOB5t4+nFEXloSk6ugwMHzLvrF4XStXJ5J/F+pRAvFvNJt7goitOFcqUUmvnwd/E7hcOdQmNPPMztC/3jlIBTjGknQSKekOzHQXKZNXkpjlDgu+uMftwxVNc5Jr8SDyIkyalmJUUsbLr/vRoaDuNOQOwli4GDbSpXQKDvZ+7qaGs7P8FAzsihx+UN9HsvF0vvHwjJa13lYgaXfYUuAiKKWQJyZaJUnvn1mxsKR32K5M4wQAKB5XPMyHV6SEZQ/LOhDBwXndmlHXLxcbxuaahudap5MDvyysZ0YuweLByBgLopzHa6Vsw4HujmGIjZ1gQQMzWUMdM3BXEtwoVyfin++Mpm5tkuj5zxeFC1Kot7VxYRl+xM3+HMKkDCXcUruXVRjLofjxcKMweFmV+v/iyoFQsXQv1nrQA5R7rxaJR4p80YEtyXyA9P1iP3Rs9kErIn+srlbGcmMZ+ra8nVPRftPYx6ngvJyWQTb1gLRGyUOdD5Cz6fS2KDOBoRs12yst/WMpCIPDU0sSJ7LueSI94pWcb00M6u4E5sbJbQ+43nhL3410NI/PrpoVxeFK8tdn115eAwHj90fz52jXIIKTzRTkxHIHCRmGmtnE1ZAnZQCtp6jH2CG/CUjOxI5GEcKQyurk8knql7DdPydR35iWwI3+DQev7E4r3mqHjZR+RXQbC6A14mpwKCkJ31yCclaBoqGRrG5CHQij7t9WFeoCBKUwldCAQaqG2FR3qynhWqPNPWzi3V2G7S090tq/K01Glay+G2FWyet7XPNoz2P6yxaBjQBRWkYTmggTTWOg3kFiZ6f7DZ1PYwfHkz7EbhBaiwsbkRptL6LnM/H6nZziSIrkADQqLLYXT2kfkvKkanXlffEWn5eCozuNTUbqO62A///7iJKji6NU4YsB6AZ96MnSpmfGR0wUbac4ta83jbv0B4sk/WK1CliuVSuVJk1z4wHOABNFVHV+eUdKedMRKdC2/FeGNLYvSS4dV/yO5HeGxGXXzwXDsFWRuLFwHBZNOExSgifc8GtcOYFQ4zxsUOK6gkgcpdLAo+rSqh07i7XwyHBU1zXxpDwoB8MWNDQgB0cx8u/1sXbUQYHdLt6c7l/v7Yg7OwnF3ZvKZGpL5FJ2mPPTiqkyAURjkQDXbl1UAgNsdIJuIP9nf398NgcVd2dzFJdPWqqAjjX5YqlX3XSQ4tDqb/CQmHukJlYdA2ti1k1vTOxiaqtI8Kf6JHorSU5GJsIAACjrbuOgOHUBjIWC+r+A7lMC5Ube+l03vbu/vhIiYfb6AtYZ0UJrevuO/DfVgHseDZok/inSDBUF25aloGBxUZGJLPAoR2ovJTB5vTYZVDbCYnk3FVNqaBBt5KC3/KGSDqW32kKqWKJQwEqfCKKQyV93x31veVDoQnr9IsUWHqw/v75XoIQSGrfVjaUEwH2iSenv4WHYxbXn7YpkaEJ3QWgDgxo2KqfJ2bLqJmKb3TYdwwpCoVJDbKn6hQVp6tkmvZYsZGDZISqFqngUB9jzziLu8JDx1d/U3rBfsGBmKlbfaplL/pNiY96U2Go4/UVS62q3V0Fp10G5XVYaMggRoq04VwGPEIA4SJesU1kBjv0DmsbImxUQ0IxzZqdJMKv6fPwZ8A8QHkkUoJw/ajY1HVQNhXr2te6bTS1sShzHPjqEvVo8a6gXTPaOt7teSTLNl5upldYhxOEyjAEdYGQBEGgnA3NNlGYzynx4N54Xj6VUk04QECgeMdLUF0EorRR0BSJUF88skjUZEwEE7VG/Ty+dtY8y7dyjYM8DSBleaYgVTR02Ha33uYMIPCRFiUU8DY5sR6H5FbVNXMQBog5Dtu9dIpRxvQ1zbISUC6UbM0DkgUbgbZLwv4d8/UkFwfwGSezuH2+V4+T+JRhaUdlhC0dLwCgkI6yMZEFKCfgdTYHKpBniI7UdMBYuSAXldAAiSCMQzEtIBoolHibES1RO8pHDoKAwEJBXHv74sA+e03ETr65puBUuX69etqVGgbxjrAJCLMSmVjDgnWElvogTW4HuivZdXQojSdlJh8MmK6j3HoEiI9VT228xfckA8gKHWjGhArSxSW5xrIAvIdmW4U+aKVZBeBtbf9CEDmjoFx9ejGDeT6R9evH+sgfe6U64E7AHLaOZS63YCGw4kKHVDakBqVg7vPdPt9KSOHPgAiIOF3wvIDaL5pAHhwMCCJpqMGMlCrB8Rg3WAg5VLpICwAA7r+4483nn/yIbEY+8hNpFStPhabx0g630TCNlOxt3iWH1YrRlD0Jq2kOTAi993+D1tHgOH0bnm4AAAAAElFTkSuQmCC", + "description": "Allows configuring location of the selected entities on the OpenStreetMap.", + "descriptor": { + "type": "latest", + "sizeX": 8.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7867521952070078,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7040053227577256,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"defaultZoomLevel\":5,\"provider\":\"openstreet-map\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\"},\"title\":\"Markers Placement - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"54c293c4-9ca6-e34f-dc6a-0271944c1c66\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"6beb7bed-dfd8-388d-b60c-82988ab52f06\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/mini_gauge.json b/application/src/main/data/json/system/widget_types/mini_gauge.json new file mode 100644 index 0000000000..58fbf1c106 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/mini_gauge.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.mini_gauge_justgage", + "name": "Mini gauge", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display any value reading as a circle. Allows to configure value range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 2, + "sizeY": 2, + "resources": [], + "templateHtml": "", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#7cb342\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"roundedLineCap\":true,\"gaugeType\":\"donut\"},\"title\":\"Mini gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/navigation_card.json b/application/src/main/data/json/system/widget_types/navigation_card.json new file mode 100644 index 0000000000..24f56676ee --- /dev/null +++ b/application/src/main/data/json/system/widget_types/navigation_card.json @@ -0,0 +1,21 @@ +{ + "fqn": "navigation_widgets.navigation_card", + "name": "Navigation card", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to open specific page using relative path defined in the advanced settings of the widget.", + "descriptor": { + "type": "static", + "sizeX": 2.5, + "sizeY": 2, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n\n}\n\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-navigation-card-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(255,255,255,0.87)\",\"padding\":\"8px\",\"settings\":{\"name\":\"{i18n:device.devices}\",\"icon\":\"devices_other\",\"path\":\"/devices\"},\"title\":\"Navigation card\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/navigation_cards.json b/application/src/main/data/json/system/widget_types/navigation_cards.json new file mode 100644 index 0000000000..4e520070f8 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/navigation_cards.json @@ -0,0 +1,21 @@ +{ + "fqn": "navigation_widgets.navigation_cards", + "name": "Navigation cards", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to open one or more specific pages using filter defined in the advanced settings of the widget.", + "descriptor": { + "type": "static", + "sizeX": 7, + "sizeY": 6, + "resources": [], + "templateHtml": "", + "templateCss": "/*#widget-container {\n overflow-y: auto;\n box-sizing: content-box !important;\n cursor: auto;\n}*/\n\n#widget-container #container {\n overflow-y: auto;\n box-sizing: content-box;\n cursor: auto;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onResize = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-navigation-cards-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"filterType\":\"all\"},\"title\":\"Navigation cards\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/neon_gauge.json b/application/src/main/data/json/system/widget_types/neon_gauge.json new file mode 100644 index 0000000000..e6bfb0ce54 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/neon_gauge.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.neon_gauge_justgage", + "name": "Neon gauge", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display any value reading as an arc. Allows to configure value range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 5, + "sizeY": 3, + "resources": [], + "templateHtml": "", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":70,\"dashThickness\":1,\"gaugeType\":\"arc\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Neon gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/openstreet_map.json b/application/src/main/data/json/system/widget_types/openstreet_map.json new file mode 100644 index 0000000000..2f3ba00d1f --- /dev/null +++ b/application/src/main/data/json/system/widget_types/openstreet_map.json @@ -0,0 +1,21 @@ +{ + "fqn": "maps_v2.openstreetmap", + "name": "OpenStreet Map", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC91BMVEXx7uip0t7////18/Cq0t708fDyyUzw7Oa92bHy8e5vz5fw7efx7+7y7+rx7uzz8+/9/fz08e3z8Oz8+/rBrLzx7OoAAAD5+PXPv8r6+fjOvsnVyNHv6ub39vTYzNPt6eW/2rPSw83t7OnRwcz39fLx7+rq5eLp4+Hr6uev1N/28/Hs5+Tn4N/Lusbo5+TaztTd09fL38DGtcPu6url5OPf1djJt8TY19Tu6+Xl3t7W4umslKrXytHa5efI3ePh2doXFxbh4N+52N7h7Nioj6bp4+Xf7tWcnbbm3uLb0dfMy8fEscCbmpjMu8htbW3g19nTxc/s5ujg2Nzf3drVxs7Crr6AgH/U1NGTkpD08/Lb2tikiqODhIJYWFcuLi6wr61ycnD01su+qbqOjYxISUji7fHR4efr7uPO3uLk3N3R0M17enmvma7g6Oiy1uDd1NrGxcG6vrOtrKmmpaLY6NC0nrGioaBOT07i2ty6preVdpSJi4pkZWQNDQ3r8/Xp7ODl7d3z0cR0dXRoaWjc0NSnssbCwb/E3Lm4uLdcXVxTU1I1NTP2+Pbb6+/j29+3orSihp/W6O3c6NXEs8K1tbGXlpWLaoqIh4YmJiXc3dqousyzsrB8fXxhYWDy5N3Hztbz3NOnw9L0wLAeHx7S0c/1zL28u7mpqKbm6um+2uKozdvVztbNzcu5s8SZepeegpw+Pz3y6eLG2NypyNbV5Muju8u5wrSen5ycgJuRcZHt9Oq/vrx3eHfm6+W0vsv1x7eehZ/IxsWepLvr7u7C3ub2sp+afpnQ197S4sfN4cOrrMH07uvDvszj6OHAxbz1rJblv0/k29ugr8OppaXU2+KTnJ7JxdGzqLywobb1uaadpKbVtVC5ydKCYIGCzqF+c05tR2y80LKB0KJtln68o1RNRTLy8eHIrVqIe1Sy5cjQvMimsrXMpKt91KH1pY6mk1ZYUkLRzMjoxLvxz2hvZkq0mEXHtJ+ol5LAso7UuGetmFi0nleznVd4V/r5AAAo9UlEQVR42rSXa2vTUBjHTzSgw7C80IH3S0VtUIuXQSUlkdVpvcx5H5udOnWJQZco4r0l6uamGwatlw3mNO6F2qxWoXOoMN20xYoUhNHaWeYn8JXfwCftujaus8PLD5q2aUjOL//nOTlFE66HQp4QjkuswnGswuL2gB0foXvA4Xjfg/1Cj8MB+35D4yfsr+lub2xsbG/v7MLyQoWuT0DXFVmW2TlzOE4cEu9LvBx4CyYZOt+DSrdB471jIM/ZP4HI39PV3dnZ3t6u23R35VG5jtSEqLrcruJ3vc3umgYYeX15sxrAjSqZkXeBxvtOLA+fDEf8vRDY5NEJoUQs6q5vqm9qqi8m9raocrUgCGdVO/6rSk/XsAZI5b3yJ8Mh/ygdkBlbh0qKuN3lTU1tBIhE5WZd5DtEYlTRc+jqcQCNWH7SlfU/dPR0cgSOXDGmvDfMhZs6CMLakpBrwoLQkqktQ9sD0OX5GVqAWAkDKA37H0A4o7JBZ2Niwt2b4J72EoSpudJirejo+B6g8NF0j89D0pit5xBCAsyEIo79J/RoGoH0rIajgMDYB3o6PVyAgNoqtpi9QwHwyEHPbz0olhbNKMmKrUuRjmAXWczA/9IBEGunkvCKSOiYvF6NwnPRCH2OjQHOT0Fpzm1diFLw4rgjoai/U8IRlY0AJmZ8DKDfsVH0xGJ6SgzKMHP6MjSCgI0FPIZT7wpHYZoHvmWseB6aS9O34wZRBngw4XN7YA5HjskiVkwk4JpohGW7Z0M4GdgxPTw8i7Msy7E0E1IidNpLlkOwi9Y0mo7S0h+IhCgNqiwIJkzO0urOWVix4NSXhkB2L0RGcowFlyTJ4xlkmaDAKtwFmmGYOk8KTZZZjhsK0cAXP80D48sF8Z4UctXdQkIsJnSCoZy9nutx3ZOIncQwMxph4RWDhkmwS/ivHknv5/2rSKI0wtGvowzgUbxRIeyROX9c5AA6TPu/gE0UXuMSUZSAAgusgBQ0gcJRK2xNZktwdH0NpDsEpwwjC9OfP/f1PXqIksw6tBhlYdbsAD7aA0TObyVnfbP5tl9gGH9dnTPo/XDhwytZHRyMcBGOsfU7W6O06uz30zQ1PpHm/UPy/dp6AjhQWVFmKampvEcQXg03kl4E46wgaFmnpi8XPejrO7js60QY3dJD04wdYk+SZaFJAgIKnaZpW0jfjVm+VT65X/XGS4NfSvnwazkyGGH8fs4/GImAiJ+PttJ0nqlPknSRt0rLNVmuPcxYqgvftb27VnGvur4EnBASDQuuLsfwEtiuVvfG1CCbOgdb0vegSGfS4hlgsnj5NpTFm6SG2SRIhjTAY8dzm+08ucnn89l85teyHI8H6+I0/QxEGDEe4fpVVW2V+3lwA5HMgI3gFKXp92WKWUPdSoPCK2cDDFFrLWs7sbnygPVJeROIANld35murEDM9b2joYrTKVqyZElRiqMlc2aQS+YhA2JS5A18EiQ826PwvM1mu0EuBBHnerIORMJeZ4RWB+VVg6JoC3OlX1S/Kl8AkWfDPSIxesBSCjuGm0yG7EGkXGGVfWGx6qzlHYhUW8xmc8OuwlSJs5leH56z8IDLFRBqybt7uXD5UhBZPK9o7cm2ebX7Dsx4UlPzBGUxJSWiMQjgKX0swyK7ncSOjTfJ03oiBWTp9k2tQW/panLLbdmvi0Q5Z1xtTYrE47SWvgUZjkyevHIdMjCkfAcRNxOs2kcQZyoqKs1PKw+XmRYhIDuUgWERSnW5rsUSVRVrObXF+vJJ072yXe6ywh/uO0t3ua3WhhOGXrcHTgVARfSadC+E0oGs2nwRkTvJTTd8x30kuee5bW5BED74dpLFdT7vgmDQ6VT7eYUejPM0reFGD2DdZODqrUkog1dRFFZggwVV+tRrssDGYiEKM7/bjb0uCa7DHR17qyp2gUjJMfgLcHhz89PCve4TRdceW601bYYbtwG4aDbDuidpkTFZc+nSkQIyjYnMQQE9DMN41yAjLyYnmf/xIhqBU36yYnY/SYVxHH/2tIZnB/DRAxyPcdIIkMKXLBLZyii2XszsPaNZSFJYrs3M1UU5IF2QuLaoFq50DXpbs82K2WaZ66JN1qy1bqp1031X/QX9zgHkRehtfTblyJFz9nl+39/vAY7D/qpBOrmATg4mch1KoxHH17O0yJev46a39gPmY4Gh2osHzUalcXSjXNnao7x7xmy+fR+l8W4Wcc9L0KkjHfJ4AVook1umg3tLE1BSKoFcp0MaSHlmK9C2Wb8/BlLuyqIU8fTpq02vvjx/jQrTJCRry6NktLqedjXeeWu/1W5uHZKahkxVamWncUzpaK9Stj54fSCzRTbP403cri+UPukFPOgv8Mz6E2WIw0Ga9DW6mg4amBvoF3Q9e7wFRBI0da2nabm9+H5L7YjdTnc69MpDMLXUt+B3AC5TVZFTEcDmRgJ0LEanYwfQf6ERL8pLJT0/65uabkiZXvQLtikUn+ZFNq2nARlF2QGpMgdLTVZPJrM1H+36EPon3LOVBTzSs2t9U9OrG78qiGSpQqFYXduV2l8NTpkO0QJI+FHecc5rXFlSAyK54aLnV9Xvn0X/gCe/Rn3II8ncfbuaju/98QQVgN7QoADWSiTrVyQqcs8JvQgqCXT0+5RGVUNDxYYdDTtWZY8tL0oR8vtt6K+gPd5QX31RGn+8Mj2wcjlFI6cT5aVq3TWFwOpGiURiEL9RGPQpE2NFVEHUG2NCY3e1paEBFl9ddTR7ALvTf4Tc6C9o7KvPrYTfLfR8pT8eaiwQn4UijdNzc9AcIvtPSBIYpiNOpeZj9/mNFKDTlVLUE6pCDx6SbqOlbBUSNiCjog6hrIn73/L0By2Wez/QmNPXiBZlg3pJEqcPUVRn9305BYCMgOlSj16pPqSXW2BB5ILJqqNH69QoP6WLVpai37KI4f0LNWZj6A+4gzIxzM3VqtVCe5SlLSI+OEPRrQZR4Mzhg1KquaWjdnSoR/mio6PFYj58u1ncTCv05etQPmQsazIRTvarQAlLyp9p61tYDg2vTYnyK1EhxiQoA9HDuF+hqEtI3JmJOCVi1SjUTVEQqdt329rO1F4YQIdeXt+rH2XNA5abzXKoiMiHfRaUB2Y8fL49vJH9TXPEpwYmF4q4O69zqev04MJ1vZc28UUED3WdQrE0WYwZZ2quUbLzYq+7Tra1HTaN3kfoQKDafO6ca8Biab2MUtTlK0kpUS1muI1WIuMJZhZBgeChBJVyBHMlMo5hCGFliBomRBSpHx7WkuFQUVGsmHDe2WKMOUIYTLTMYhUmJYWaBJZ9TDx44hsbmwGP6v2K8kZR415GB1HoNiVwDkRaqA4zQp2B6uZRPQKM9wdS/7luF51HhFNxbAnTRhjDOWsrt5IZD1oPsCvJ9XD4PMPzly+Fw3c5d/Hh/g6XINI3fvBC/9D0VGyk91w3V8z2tuGA42X/YdZgVUVP8igvzsTKO+84xc/fTghWg2K1EZ7zzUhQJrr20d7eZipw5nWnlOpuRujWgUvo+Y0Hl1BPj8NFCwMHRvySXSgPpD3YA6vPclEDDgxgTZTCjsv47sQw6RzFi1UBDNUiEy3EEBVF3kXn7AcnSMx0/dvz6CR38B0+G5bi7rOYVzF4EcrHE2eyF3z3EhnyVdco9lcLHmMoB5v/dO+3kOH6SajL3goo4TjY9Jw1o723HBbLduSx2TxoydK8M4sxj6raOXL/Isfx/cyNM5yMNOOhcb6U6ecgLzKsKtb0M37oEVGkfcrP9U+Pt/iZmOsyc34Sn+3ltIc6GKxijXljNTbzRJLkFBKw7P/+XTF4xxeBVOXgqRfxUiI+lIVl+3a3DchfERnLcQx70YVvRCcA/mIPj1ZyOFy7CJGgFEQQVk2NBJmitEjRcNDs6LBh7+1RLIg4WO3IAI9V/J6a8rLBnYN1looTg8tWr0YCEh+MpCQPRZENMK++RyIwbxcSS4jEKJHPWQsyWFYesgmgijK0kJW1ASIrYVT8rfOYYIxbHJyO2LHLrJWRfntSRGMlGRWpJNbp5iFOG3rgSIkcusBiFVMi0VcvB5aUA2JzRCIz98ZSHoZTD+G5tcLcpcegWfIQzxApLc1SXVtes1/08MJxzY6yteqcZPFWEybNVmy3Sgl3HZvCDGlz4deXCQm4cFIET0wyVLJHgsXD40HCRc3spGmYCCIPpFtHWhnQxotS99wpfklLV2g00/XxeGMyWKc0CMHYVSQjXlikT8jVm3soA3VZ+doaUcQtXNlYVbakAmXBbgxGrRMGjjt0xHWkjSGXwhAwljyPBod4LikyZQhaXYcTIucmjlj3bhyZDgZd76bcIPL87Brl3laGa7fOT61k9o0ajSYWj8caJYKK+iF4rAOP1FvYwtGyUb7POSXbINTYBni8YAJU76pD2WgJIZgHI4gWK5NxmCFaYR+BBxnBDGLwcKWXMLCTxMVoYTs3N3k6BK01UtmHiYlgbQm8spTBuVNLqIDbTf/k1UxCmwijOP74hmGM48RZYjMz2RyzSZqYGNtYsRBFBWMsiTV1xYpVkKhQd1slqCDuWHFBQZDiehFFEHdREUUREY8ePHgRFBT0oFffJGkS25ombfR3mTLNYX553/t/bybDsLNuMWxWMIUeJ1taUn4rDMqJnIh774CFl0wwmAUoAnkRYCEQhT66NLsfQBTzjV96cHOQg82O6ONHrfw4Kp9aHz/evn1qEw6QK0+dOriMLeTGX4dFDH8dxoljYoi1xlrQJZZmBibcnlyL9L7N5bLgKnTIyZOA7MbwLRLuAh3WG23WEoGAH4YgNyueWrn2xK3JW9/Mu1+4FTwBCzcug4qxqiZTfBogDofVE9O0NJTSxFFiU1Zk4arnNwHx2SxNkKMlJ/Jn96sJFsAfTGqaFmIdzUkYgpd40bdPncv11v21tyYfOIDVOIC3UVVRj4N7xAMFmGnBL1+0wurouWenKOGV3iF+fz0gLltneILbjQ9v3BCWBwyKKiIHIhrSqF9bIMBAefZgOc6dO+iFEhioFg/O7XL/1vCHUinQUW/Uj6MoytCwZ+vL+2n0iPgpm2uOMU9QTkN/QiridwBjtQKCIl4YymSMJC2lYUT44yZTMwsDkesBuR6jaRTp2DnbMLZ7+vLjTz70jLOZrm+Y0LVt9jGjca4sswNLjECRfYEElIWTSBaJg2poOgEl4HbeFh30Y7IfgO4wUaJIUbNpY482dtv09Ovgts7O6bHuFzHn7E4jYsGSDIE3UL5JGIXMPJzJ7FhGJKicR4/elIrsw2cMfhhcJIEi2zooUaBau2mqp3vs8cBrF3p07uw8HmhtPTnOiDTIchMMhWopt+IX4Ph7pqHhzIyZxAWVsufEiROziqlqwTavh8FxWgCong4KV1ak20D19EzY2bhKOI4i05tbdY77maSqtmgsDEWXKaxHwrTgtP610lHI4TMEOXOYSJXV4hHArD271xZr6iy2+UDssheo4IY5Ri9WZAp1nN6/wdPbq4ssj+kezhcak4hEIo0wNCmTiqNec2NzC85ixdi3M4AQkmkgSEOGEKiACf2/OQfGroX9ewrIFlagLDdehCkqfv0kRXU9gN7ebW1zjcnpptZW+frOKMNYIpHKojGhOXUDv7fLEg5EcA7XN5wo5ETqCJI9wDDwhk1t+6AMduwSUTAaqTwCB+d7zUV8OKc5WiJQIYXSs9PqZVl2as4U1ELkXdzU1gXlcCTzmx2XBXQunLehDwsMnmR0uiIwDJj6kCwn07UQCWJceaE8jpQ8INEeM3+QTkZg+IxMpFHGMmObtziGXg32gbvE5VIPj1P2/gcR9tOnficSzlCyLQxenEpSDFRAWusfSntLRXTR/yDyqZ9I1OlMNnoAAHfzYKVzwL4OVQt6oMi380WReBP8B5EB1XA252819KG9chwJWS32ivhNKHjEAP67CN6pYTHy4NBeFQ5Myjyc75svrxFta/7fIp5ocyPTL9KrIq32JYPgKywtU3cHC5XiBWDjDhvFDV+EjaY0jOwRwah9NaRW5TysjUm5ufIFyt3Tn+PZcBulCxiqFAlpWrAeRojcJyJc2JvPKzkBlWPArNxptsXiKBMMtLTSXW2BcYbqRILJKAsjpbC0uMcXGMQvq16oAio4+0Xc1hPp7Ok0d3dY5m4IB09qNFeNCJvywohhVRb6eJ/NK1mur0rEeO+GOd5B++Jh85NOmt5w7NixJ7TYJ8J//vr1Mz+YSDBaaPRpUAsRKIr0okhKlh1Vicy9122Oh32+cHzBDZoedwNFjhdFPo9GPvcX8dpjWqDvfslRk6xnSkT2PseH+E5ZZqsScdyzmONx2neXOTYbRa4fi22zF5fWV13kK4r8oeF02qdZg2qoCRxpexBqQsn3f/PtBcabwlavBgPEx5mbps++YaJNcUysyIblbWOKzT46yx8iCacW8uTmEVWWcbaoDSVJKwpvmX2pd1AVHIDZZXBb02NoUaTzcGVEEqkuK+RgPR4WakXUUrwm4f2FVCpVfaK7DQZaRxBF0WgURTf8XSSN6fJPsMoh6EO48CMlp2BYcBwUKSOiqvCPiKr2wp74/LXVb2JhePw/EU7IwvV76yKoqumCCFhMVkBE6h+JpGsggg9hERuZKAlQxOWaq1r7RHywz1TfhH/xY5XCD2wuroYinhqI8O1rVqxYsYTsmOmCAiK/UdkHxYpAoxqJqAumHuYhhzLTaKuhCFsTESMvSRKffXeCJz79jQsXL9URyQ0g8fiP86tAUAixeTsJkfQzvNlMFl3hfbUTgWS6FiI2UeSURQvJoi1H7pAr7Rd38VPx/YmlAnC2Q+fOHvn+WuBPXzx7ULJNXU8mrt98sX0ZuZjJLFFqKWKHCmDKi0wkCL9iMVmxY5Iy8axEFm0n9AxCOODougfSzMxrsvohr6y5Sq4dJq11T8m6i4R/eIXYoHoYZkQVSQyeVyLn1kXqEJroIosVV8PFMUQ6qhydgR66CBH5hz/5OpvgmzmDXFpBFrZLVqVOkhZdGY7H1M2blw0m4uiAinA6AOGQUg96yhjawPHtz/SCSDkRN794R/tm3ocikBMB8/xfR/Eg4uUvXkEa2hWLUqcMU+SOJJ0qFRH6UqtCEW2BWzTQBQwGEQDoYz3dMSPNt08wTaHprMg1xYb9rqzfRVAEtQ1L6wgnLTrFZxSRmpAhWRHegyLKsEUUci6TE8mcI0QEndZkCCqB/l3LmcY0VkVx/OTdPPHZ9rWviAUKrV2gTSltbVltFYhtUqAUStkEBATiAigIsqpRwaCIASIDSAbUKIqJymhiIiOuH9yNSzQmxn2Lxpi4G7cPnkdboFCgrfGXmTedxwzc/z3n3HvufefdpJCC5Co2+Al7O/tsVfkYoVr0Wjog5PYXUmSTXorqf5pKuYGSXMzQF59jkqTfIKQahiSS5o6gEFHiOSkXN/dTsphTFXQtNdXa8kVAyBctJ4NPSKrkaRAN5WfTttlTc7202+mcoQOJNTD0S70+3yYKKa2Q443hLhTSW57Y3NIy7fNJvMUP9zK075y66RtOStJlzdP3NyfKdoX48ou9iRAlUoJw0m1FlP5+1BFUghVd3LYQI0QnpIcuXSw129kZZypNz9pstSiE852uLbfP2HxUYsUgTVfgHEFJXHMafqKYmcXyPImNaUfXSpSkMMxzEr6ej0O3w1+QSCXKaSpqi6BXohCGoxkc5amVL8/a4cu67W9SkAfRgYvnuUWbq2mm2umjy10224aLFtGlp2trnW73mMNRNcpUjKJRNmiD3Uo3mRXyWWH5IMM8n4RCbLRezby1f4OqqjRDBFHCkIZ60tgsIoiEavlkV8gn05Rk+8XfaIVY6blBuqrJZR3F1rpttk0bzdDZm0Tbk9o753DMa+i5UppWDDKGnhn6JYViPpndyPPZTvtSnpQwQl6IFsIQVFXGECCk4BKTfwj81y0QisJI34V/+ibg3z6LDmetcAx3MgbpvLN9vJDZeRoZnffRTfPzVrnjlI+2C2m6dtBssNO0U6Fo6rW95PO5Ts+k++RMkr4AhYT3v041EsO0KyULyk7Qta2tPX1QiDHZAdHS5LK9hEJKafO8i1Y4nVaWzuDo2VNOZx4Gx8bzGOtuVJba3j6If/S+NFZLC+U06xq023tKGb1ef0BIrio8PJUPP/roGzo4DCJ8bBn6vGtrT6aHu1YLJbHE8MjFTfMDsA8HXRt2vA0/S0FKHw5DI0SbkpJCSTDU1QeFKFRZQTNsF7Y1Pvro+/73HzUeahLD+DLkL62tDTVST+8N9qcpX3IpRE2gdTzBhvKtYhn6KFKoXdLpF/cJuVKVBojp5XuWcwSA5lACgPxQm4jo7mUoeXptzdNI5RTvmOQLLBp0jwjgf8QgoSRbbdPF5xRPt01ihv+AFsJ57f2SEpPgWZ57nn320YcB0b0hh0Mgt/mhYNnbfYJIsKz3k6COu4ZwQvxfSaeo21paPv3qwzM+/OrTlpbbcK2yX8g2zwZpDI7Jb6QdOnAZoIjwaCnqyeIvUconXxYP8SnK/wjqSPTe9cMZIX64y5tIpe8T8qhRmZkFAlPJyy+/p4cA1fOZcCjLqgyRFBc+hKJK2u4655y72kr42vf/EwOVOPz122fs8vbXw/uru1+bgQhYj8iZ/DU7ldp8ti3By8WlCfA/gpWg3q/PCOdrLyURhQn56AI4yIj10UNtYlFACCKToBQZUdbIs+B/AovJn6JuuwvtEcbb9+9bED76PlwYSYpqVAcRKdJE2HXNUmkqBfD/cOetEkkLxsc+fmiRUHtN8j7AQ1dHaoPVmhBZR1Wk+1iBIC+AmCgwRrc1fO211FZLqPm//LKjpGWSMkCAtNL33w8ciRLJJGcr4SACjSbyolyXnKyAWDBq+AIdXRSCL6faPg02/veVS9r+DH7+tG1nCJbLjcBz/hURuib3bB0gSgfWho0odYJQqZji0AqT5FsgWtIysTgnF/9LeVY0Y+/0V4G2/3F/84Md9/8W+MtX06HJS6FJC71v+kwEZzk7CxCHIrCpN1JRkcm3QJV1WNs0yRaIlmSkEhCzEY4lkSr+MOBXd23hELlWHBDyYTEl4dsp33014KqI4S7PNmcqz1YExVrkRgsf1YVwGLeoohyDRQZAcwRD7hZBNPtngaZ/00KlfCShpoMmwT0PlDFvBuQIi4AyOTtbXhQW6HAURbsGObpxRAs1oUzZ6AgzScYhFnk7aAM+cbzjhoB93i6mEs+bn5fDHiEPQSQSdMo9IfdgK4BGMDQkbniB8IgRTszwc7woIGRX56wRkMq8cEOEkhytAczJIYd07DGJSCxmIki5g2oJxsiPq3dQd6x+vRMjd9x62zV3wg5XQRSYTrSVcdqE7mai1xMiZMlepIBYKiGEfXD7OhpuB4OBcMSgZbVusCTv7ljuCMkQ+9f8ejEnEu0P9us/Dc6CPxY/VhzKVT6doC6/5pozz9vjWNH4dXrr0ytabd3T3czJErLe5m1bI/4J7zoxPXl9FyGAyHcHLacdkKYeQM4eK9++lfvS89lapOp56yiYzcIgbIIypIQR142Pj5eJw0ATGaiT02cE+fA39KsA0yeo/Gs+P3fnJXn1hZdFI0TW/+6SUOvteFrf4SG395nqGtASZdcvd5Ut4wfeIcw7hcSCU025ubmWpiYAeF41+NIceuZnTnjnJYdWm213uxZh8sEyobAec1W1sM7TBwVdXQnGbIt6/MnlrTaxWG3SM2Lx2iQrFr/QKpCKqBQctsIIBsyb5yK33rgbIceTIVt/l2fgaf11DWznGt1cx+Z3T031da0/GRSiVBWG5rrNjz9DFjcLoEeBfFwEhZ85AOx2rWHR7XK1o5CVLuFUM3QvqNs6TkB9Q4NXqFb4x1vVk9eJxes5J3PE1w0M5IvVtw3dvsykUBPoW/v4dYJ6CmXwSgQ750hEIYUMvNs/tfLuu93ijgZ2mCUddY1Tff0TXUgdEQffpw+lnJubbre7cHOzQLC4LcQCuZ8pUcjzWtfHLpcL/11hW53wkgGhv9u/9IIJQDc5LBQmmcY9ePiJ2FOfk9MsXBmoF6NxSjrLxITqKj6QNN7wCPXEuQFC8a6/Sh1FsA/050xN1fc3kAUPWSKkdank+r6+8YAQKWyTF0ohT23ydVGbToAxBbKoBOPHfNCc0tp4IaXQUMcLeUEo6NYvPVKQ3716fSfGi3q8tWNlUt2JQh7LZx/0qoXr3d2dfjGm8VPf7hPybSe+4BXkWogBaVeXXp1E/H2kr5HkEKLPIX1bL6yZGk92PBLa9LNCkHZeiHYzGWB0rF2hOA2QMMYH/PNaZtHlKkyF/K2264SX5AtN3YynTJk/PrnVqcfAH//JXzcgHkIhHWLkEtMlW+pOpRjDPef+MJPgJFJGXRsScjlER+P6wBoASVermeAEglxMdmBEEG4RKGh3ZWQYy3njNH28YQckm5/77VoCY6deGoPV8fFJ4ZReWNIt62r2+Fe8w8MmoVDbUMAK2sTovHVCcf6S16NuHh5ezWf4ObH71/AIGaZSgjJuvQmiZD090RP5IIUMEGXALkU1xx2KwEtOq3ZBmhoNsH1iUGIimJiMEj+erpXBe6iYEQfx+xkxm29ixSIAjkq/65s9Or65YTlkkFcKIFoGhCgkujKL6J5MiEAYzu7JQbliXknogmDuAIiMum16j5DpumCEnHdTbI/Z8mE/nCEJezCcrOQ0+G8UXXn4sv2S3Xj/9K47KOmr55776kUxP/hEpCLohhC0oPnplZOydC0n2rvqKoIYoQmhpbvumes4fCNFiM4VcqwyKgXiRSwuaezcsYdpqqO1sftBGY+WhJpi0USZybshAE6lIgFNuFDimaYRAGxBJFKoB4M7EG/ftcTvoMSJiHgnvJ2Q5WkbLxETMjBlMplum5KVnZjwyDpWV0NBUhmlECFJZwAYcrK7YYkmu2iUMDkAIj6/FB3cE/oxkAMPSygO4oX1bJW0dsLQ9Q11XkJIsxeF+G9nB5qV9c0Ny8tPhjb3ISpcra3AMCK6cSKHIHRjPkv0gLCgHBhSEoOhq48QCINQKY/xYfItHsCghbjRe7uYrk7gT03j08X8bhNyiXbAA4XNJ5eXh/3Akxll3Vmrp3V1jWFIRxlN0CALZWUL/q3rC+F6HZQNPblqKFlaX+8wcBCGDN+y+/uMf4qXqdfvhrhhx/tNJ1DIegnhUbehjpzbZShEcF3X8vLtukC4q4wQDcMPtnZ4GIbtLqEZMWlsKCsbaG2dKCnxYHzk3zZM1hf86mGT+ECYbBX/VbxFSZ67F+KGGfD2j09B/e3blRZEPOTpH7hkWdY/AFDmeeTJB3fXu9Hw2EpHxyRvkT4+QIbQIredbJ3Qaoc8kwsLfZ0lT3powyU5ZH+cpuC++TolkV76AMSNWL20lM9PJ1v8RURI2ZBHnS575BFATvRBkAJFJkTBar2JZRgpaVztYlBI920vrDzSOqHOuc7j8ZR4Ov0n+w2GOhMN+3BQiISDu/Fl8DixOPZNVRzZRkYgHKM5Kvs2X+dtZIEjj7R1r5aQAW/bCeGJtomJDpYsDU8NGRonDYacEinsIw+PN0jBu5e+eDPExy3lGogOwTVl/oTjhSAZge6ox3GLwd+kqw6viDzXRwwmAyEGKMjSvZNrKbWMFIAgTWm8JdRJj39wX5zhnmfNgii5BunLFxwjhM3YraXgIEPMjx8LDKGxbEaO4CIGr6k71Mi32U0cHojTJOUWiJY3r+HpgtiQignCJ7pwpfyWkSKLQq4o3OuwcqTGuGPrx/EUqDionM+CaKlHGQjEigg3yIIhnbXzmDn8MLbS1Ozs0pHMYKk/xJ4wplWkQtQUBEwC8VN5VOpZaJYrKnUC0evcA5cKYsmra8urdA5rKcSgpJ6X0meCeMlyAHKUFrOZ46QAMcwmgnl5UfLZVmsmxEb+NdeUQdzkwdEkGTKLXsdKfe7Smx+P+mkMxEdm34MQN1Y4hrmNjXIOQSVhCTqhCY1wDMdIUSgi4skAcFSzCMSDXFHwPwmxnC6d8dm4AHs7zyUkIYQkDJubtdliFXJ56KmE+X8S0mSmhTabrmKuB4XY7c7BW0ZnUcao0+ks6GHIldlEbp8brL6YjJ6qrSIz1YSc7XaPzs1Z2SSIh3dSIT6KFHAkL7kIa3MNjjKMneMW0/I2mwxzt0D5bJV8TLqRlTSnMOfZbFgOtmgwzNaSvGwyg2V5VpttMU7fKqiA+FAojxEiJDRDxnxCZlFaOshduWn1jdXAKULcG1x7aWY57bTZbKfo0naDYTRPuOi21TqTmmyoLU6LGB0QF7lKOJqxaoIs+mzMBjeYzOnmfO6xXLALyZyba7c7aQZbrWmia9sNZNHGnna57KnsnM020xSnEEWcQlLT4Gja5TYU0uOSb+RyPS7OMcoljanAOW9bdHO1TiwqtPscG4O0vN2gstN0U7J70ceikQZr440RR7S5CYRRIYBjaOrxoZLTp0c5TsFx1dUc164T2Z092a9z7tNCmq529tTaaKbn+UEs/Wx/vimVZXs3Z+McfhFFdG+iEcKELxmi2QwjNDGZuAP43HJ6l6TBUrwUJrEBkiBOEizHW4OjyUg1oXHmyggeQ2CJwoYIwJ29vZhySYNw996XyVVaN2laBBlSGqnOnrfytZJQyuLJwBkQP4LDhy2+SxHCA1KyTXp6uhSK5EqImrCJ6u67bwbLLYAt50Ep7opeXhVA6GXb/yNnmtHUztBmlWp+hJBZQuSzGlo6aFclK3OV/815E14PdD1aheGCzzsKayrT4D+RDYdhrxqcTTRnq1SniW6TXNljrwSctHoUXIwJfyHs494P4CA6FZZm/RdyjYeOoMxge2JTj8s1RjKbyKlFhmkfcbtPV8coROA40NURV47KqlSUHD9pjgSITDJpT754Q+OaOUXsabZFzDZ6erG6XyiNdYg3wy5HrBx1qXL4LxTmQmSqSQ89slnhqh0kG5nVi2aGmetVtNuJCGLjSk3lQed6TgAH0CTDf8GYVwQRYYmLVDc1DTpdmDPpFmcYprfHbp8lEKtraTQH+urx+x6I0BJVEfwHkvMqHJZCJRyAIGfPAdgI8RECDJLkYhkGYiEzV1E9o9GYD3z/++AglvjLR5UWvrNGVCo8kGvkQGk6Ic48EgQwFEVSFCOK7Q13+QjLuhQa1TsJ+wL+0ghRIh+JV0dNYEjRWYyZiipL2j4l24u4YGxnpWp0sdkiNdtqTZX7WB6fRVXzTlrYyc4RxuAseSXEh2Xv+cFmVU1lZni/SRnpjgEKVckW/HL0MW5FXPgEkzAsYsvVqCxBz9HxXXaz4GCQyI0QBwW55vCB2FKDLlaDPyQyaZWq1NTju0y047V4kanr9VpZuoFBMTqNRlWJ90dUVqsGIm0U5SoS4tqZskT0iFsUusNdMQtd8Ri0raHhmxfiuW6pbkuGpCPoptY8tJM5l68R/WCfSRLM6Lyxo5QrDmlTpkUOh+IwHivkdr6BgpMePgVIb8AHXVNa2SPv6gPBxhYMLAyUQSqa5LlL982HeQqIHYEZdRyGWVGYddiXSuEY0h/jtQqGFh7Tc8DwQi5JZ04MrfK93ZrF6oa0KQNQka2ES996fHdWrsjLVl0ZV4plSThqkVKVrIKIFB57lqd6OguQrobH1gwgbGj2jjfKtPr+8f6LL25rVmcI3mVlA/BddiXc/dauSVTYrfFRA8eSAZFIPS4e2ccAaWhbeMxvEBkWGhoZmey2pZXxd4UXd+SwGbA2sF4GluzsF1988bndHDzemVCZd7wMJrIQ5XGuNZWfCXD9uqszh4Csf4HFSJ+S9Y33r11Xz6KQgGFRyOOPP7DjAlhBHh+lFXAMYkiMZJK0iuP6TvvC6rC3oGN1ZSJHBAb/IzhgybauX+o4ccLb0c/CrhA0rSAg4763vr/y5vvuizDXSznRMQtcy3EGSQQqUXxQivHY85lwOazPAoER618AgCMcXmg1GyDYG+98h0L2ppA/6zIff/zeAzMLRwgdrovkc9KMPVOf5RgZ4kTqXxwjVydGn1AkAAAAAElFTkSuQmCC", + "description": "Show latest values and location of the entities on OpenStreetMap.", + "descriptor": { + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"openstreet-map\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"OpenStreet Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/persistent_rpc_table.json b/application/src/main/data/json/system/widget_types/persistent_rpc_table.json new file mode 100644 index 0000000000..284dbd8c34 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/persistent_rpc_table.json @@ -0,0 +1,21 @@ +{ + "fqn": "control_widgets.persistent_table", + "name": "Persistent RPC table", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays Persistent RPC requests that match selected alias and filter with the ability of pagination and sending persistent RPC requests.", + "descriptor": { + "type": "rpc", + "sizeX": 7.5, + "sizeY": 4, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-persistent-table-widget-settings", + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableStickyAction\":true,\"enableFilter\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"enableStickyHeader\":true,\"displayColumns\":[\"rpcId\",\"messageType\",\"status\",\"method\",\"createdTime\",\"expirationTime\"],\"displayDetails\":true,\"defaultSortOrder\":\"-createdTime\",\"allowSendRequest\":true,\"allowDelete\":true},\"title\":\"Persistent RPC table\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px\"},\"targetDeviceAliasIds\":[]}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/photo_camera_input.json b/application/src/main/data/json/system/widget_types/photo_camera_input.json new file mode 100644 index 0000000000..ddc5744f44 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/photo_camera_input.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.web_camera_input", + "name": "Photo camera input", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Simple form to take webcamera image or upload photo.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.photoCameraInputWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-photo-camera-input-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/pie___chart_js.json b/application/src/main/data/json/system/widget_types/pie___chart_js.json new file mode 100644 index 0000000000..1778e4df8b --- /dev/null +++ b/application/src/main/data/json/system/widget_types/pie___chart_js.json @@ -0,0 +1,25 @@ +{ + "fqn": "charts.pie_chart_js", + "name": "Pie - Chart.js", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays latest values of the attributes or timeseries data for multiple entities in a pie chart. Supports numeric values only.", + "descriptor": { + "type": "latest", + "sizeX": 8, + "sizeY": 5, + "resources": [ + { + "url": "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js" + } + ], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'pie',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n }); \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-chart-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Pie - Chart.js\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/pie___flot.json b/application/src/main/data/json/system/widget_types/pie___flot.json new file mode 100644 index 0000000000..ffea136698 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/pie___flot.json @@ -0,0 +1,22 @@ +{ + "fqn": "charts.pie", + "name": "Pie - Flot", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays latest values of the attributes or timeseries data for multiple entities in a pie chart. Supports numeric values only.", + "descriptor": { + "type": "latest", + "sizeX": 8, + "sizeY": 5, + "resources": [], + "templateHtml": "", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: 'Roboto';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n", + "settingsSchema": "{}\n", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-flot-pie-widget-settings", + "dataKeySettingsDirective": "tb-flot-pie-key-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/polar_area.json b/application/src/main/data/json/system/widget_types/polar_area.json new file mode 100644 index 0000000000..444b7d77ff --- /dev/null +++ b/application/src/main/data/json/system/widget_types/polar_area.json @@ -0,0 +1,25 @@ +{ + "fqn": "charts.polar_area_chart_js", + "name": "Polar Area", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays latest values of the attributes or timeseries data for multiple entities in a polar area chart. Supports numeric values only.", + "descriptor": { + "type": "latest", + "sizeX": 7, + "sizeY": 5, + "resources": [ + { + "url": "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js" + } + ], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n\n pieData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n \n var floatingPoint;\n if (typeof self.ctx.decimals !== 'undefined' && self.ctx.decimals !== null) {\n floatingPoint = self.ctx.widget.config.decimals;\n } else {\n floatingPoint = 2;\n }\n\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'polarArea',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false,\n scale: {\n ticks: {\n callback: function(tick) {\n \treturn tick.toFixed(floatingPoint);\n }\n }\n }\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n try {\n self.ctx.chart.resize();\n } catch (e) {}\n }\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-chart-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fifth\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.2074391823443591,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Polar Area\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/qr_code.json b/application/src/main/data/json/system/widget_types/qr_code.json new file mode 100644 index 0000000000..1a3828c074 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/qr_code.json @@ -0,0 +1,21 @@ +{ + "fqn": "cards.qr_code", + "name": "QR Code", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays QR code of calculated text from configured pattern or function with applied attributes or timeseries values.", + "descriptor": { + "type": "latest", + "sizeX": 4, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.qrCodeWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-qrcode-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7036904308224163,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"qrCodeTextPattern\":\"${entityName}\",\"useQrCodeTextFunction\":false,\"qrCodeTextFunction\":\"return data[0] ? data[0]['entityName'] : '';\"},\"title\":\"QR Code\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/quick_links.json b/application/src/main/data/json/system/widget_types/quick_links.json new file mode 100644 index 0000000000..cdbf44e631 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/quick_links.json @@ -0,0 +1,21 @@ +{ + "fqn": "home_page_widgets.quick_links", + "name": "Quick links", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Configure and display quick links to the platform's UI components.", + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "\n\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-quick-links-widget-settings", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Quick links\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/radar.json b/application/src/main/data/json/system/widget_types/radar.json new file mode 100644 index 0000000000..4c7f9b2560 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/radar.json @@ -0,0 +1,25 @@ +{ + "fqn": "charts.radar_chart_js", + "name": "Radar", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAR4ElEQVR42u3diVcV1xkA8PlXasxiNpOaxJg2S7Vp0iZNm72nSZqYJieWtNEkxrgbo5G4RMUFUHYCiKASBEFEVJAHuARlEQQVkeUBIgi8jbf3m7k4mbx15r2Z92be/e65J2cYGKOP35lv5rvfvZdxYwvYLl++XFZWNjIyQr60WCzl5eU5OTk6nc7lcuHn468x+BH4a06nMzk5ef369SBpfHycnNy6dWteXl5TU9OWLVsAHH5KCEtyq66u/uGHH4AXf2ZoaGjx4sXkTHd394oVK/BTQliS2/bt26uqqg4fPlxaWjo2NgZn2traNm7cyMfEuLg4h8OBHxTCktZWrly5bt26+vr6wsLC5cuXW63WxsZGuIeR79rt9gULFpjNZvygEJa0tnDhwt7eXnIcHx9/kWubN28mZ2w2G8ACbfhBISxpbdWqVfAgRY6TkpLq6uquX7++Zs0acmZ0dHTRokX4KSEsya2goCA7OxsO4JUQntkHBgbgLgUHPT09cLKysjIxMRE/JYQltun1enIAz087d+6E+9aSJUvgKZ6cvHDhwtKlS9euXbt69er+/n5ykj/AhrD8qoJXP+EZePsTZhygQV7UZDIJz7S3t6MthBVEFcQ7qRfCGyLaQlghqtJzDW0hLJnvVYFhoS2EFWIEDAoLbSGsUJ6rxMBCWwhL8tO6SFhoi3ZYUt8BxcNCW/TCCiGzIAkW2mJQlUKwKLfFoCpFG7W2GFTlr8G4zSFBg/JRd0g173TaYlCVvwY1M8uWLau404aHh92h1rxTaItBVf4a6Nm1a5fwTDg177TZYlCVvwaTKRISEkpKSiD2QVmfO+yad6psMajKX6utrd20aRNUYqVlZr8YX/FEhmFW6u03dtTanFNKQqh5p8cWg6qCto311t8kGvgOX7rDqHmnxBaDqvy1rq4uckOanWkUwnoi0+gOr+adBlsMqvLXMjMzjxw5AgezMgy/gpXOzooOs+Y95m0xqMr7WnIAk1ThUf3TDanTd98WwvpzfIUsNe+xbYtBVd7X8l8e67LP2MtimpNtmpnGBsR79hhHzE65at5j2BaDqvxdW9hum57Mqppfaqnvc5S1DL2QOwZf7m+zyegjVm0xqMrntUmNU2+Ciyonz+gd0AHW6uMjcObNIrO8PmLSFoOqPK51uNwrTk0CoLuSDN/VTakisIouDs3YywbEq7edaIsKWHKpsjrccRUW9lkq2bDzvJVXRWBBh7AI391yxiq7jxizxaAq/tqxSReEOXDzYIoxu8UmVMXDymqxsc/yWUaHy422YhmWXKoGja4X95sAzeOZxkOdnqp4WHDwu2z2x2p6HEr4iBlbDKqC484R51PZ7MPT0znG8mt2b1VCWF+fZKPhZ5UWhXzEhi0GVTX0O0iO6qUC08kbvlUJYYE8eK6/b48BQifaijVYcqmquJMCfbvIfLrX4U+VEBb0VwrZaJjTalPOh9ZtMTSrgid0uPcAkQXllvr+QKo8+tazbJbr1YNmRX1o2hZDpyqIYds5HNMSDV+dmAzMaE/Z+XzdFXJ8utu8o0i3Nqtixp5xuPzKqBNtxQIsWVRBpmDZnRTohrogqvbXXX178abN+SfIl19uz/su+2je6Y65O1vhT/i+3qq0D43aYmhTZbK5PjzCvtZBDj250RpYVe0Ny4LvUpYlHiCwjl4afn/ljvo+NhmRUDPA5iYyjA6nG21pGJYsqm5Pul4/xKZAH0ox/thqC/os9X1uZcKhWlBFYBXUX1u4JZuPifcn9MAfdaLbHgEfmrPF0KOqd8I5L8/ElYCaijqDqyo63/Pp9+l1vVYeVk51+xfbcsl34fzT647BnwZDQJHxoS1bDCWqOkacMA4DDp7JNZZ32YOqAjdx8Wk//dwLxzys3JrLn2/NIT+g65l8dcnOu5LZUcVRiwttaQyWLKrq+hwPp7KqXi4MlAIV9rRjF//x1eaPv02G/t7y7dDXppcWN/bBIxf5geMdt99dtu21g+wtEAYQI+ZDK7aYmFd19JodsuRsCvQnc22fhGQV3/k7Ftyl4OG95KIejncfboCH+oRzbM7ir4WmSPrQhC0mtlVlNE+lQOOOik2B8ul1b1jkMevDNbv/syH1k/V7jzQP6vqn7oVtt5xoSwOwwldltdnEp0CFquDVL/DPNPTbq7uM/JefHGWTF19X3IqwD5XbYmJSlWXS9vVJLgWabNjYIE0VxLugY4XCvq/NDv+jR1PHe/r0aEu9sMJXNWayzi81T6VAL9hkURUAFvRnc9homF5zPfI+VGuLiTFVQxPW1w+yqh5JM+a32eVSFRjW6hr27vhxmTkqPtRpi4klVV0j1rlcCnR2lqn4ik1GVYFhVV63w0Qx6PpxG9pSF6zwVbUOWp/kUqDz8ozwm5ZXVWBY0N/gRorSLtqi5UNttpjYUFXTPfkQ99oPKaVTN+RXFRTWzp/ZN9AX8k1R9KEqW0wMqDrYPnkPN2X5XyXmOtEpUEmqgsKCqdKPcPXNLTedaCv6sMJXldJokZoCDUFVUFjQP+UmJK6pmYyuD5XYYrSr6lJb2wadhaRAV1ZbJBGRqkoMrAOXuYRWmtHqcKMtRqOqmi+1/6+CfQG8O8mw7axVaVUi+9xcNhqWXbVH3UfUbTFaVNXY0v5+iYmkQPdetCmkCn4sqfTsusyyXcX1UNYnrHlfn1UO5Q8wtuNxyTe1k9zqNGY1+IiuLUZzqnQXO14uYG8Mj6Yb89vtyt2rlu4uhFIZmEYBjL7ake9R8w4Vf1Bc6nHJ8W47vEZMTzLAvGrKbTHaUlX1c8dz3PjJk1mmw1dsyqmCnwRMUO5HKt9hPgX8V1jzDrVZUKrlfSEU58BfD6rpVeIjWrYYDakqPnNtVjqr6o/7JKRAw3+ugmvBEAQ+j5p3qAQkyISdLKwFAwDq8REVW4xWVOXpumARGPidwSzk6p4IqcqsaoEC5XeWboX6d++a97e+3Fhz3eSd0Potp//CoINmW4wmVO06eeNuLgX6fqmEFGj49yq4EEqQoVgUKpJBkkfNO8CC+Oh91WeV7CM8rN6mKh8RtsWoX1X8sd5piVMp0AZ9hFSBmINnu/kv56/eVdo04F3z7nt6Tye7hhZUlprtbmptMWpW1a/Xf1nST1Kgq2ok5zPDuVfBIxREQJIRrWi7BYYg6nnXvPu7/Pl9bDbkcKddbT4iZotRraqe/oEPDgyRVRu3nbNGTBWfXs860frvbxJhauFHa5Pg2GfNOznJH/B9vW6SjF2q0EdkbDHqVHWtR/96/jBZtREmRERSlbDmHd4ET10zeCRCPWreoRc2dHnYgklm9+5h14boN7jotMWoUFVz18C8bHbha3i92i8lBSqLKqk17+QN0dvWO8VsQmvXeas6fShti1GbqnNXBuaks1uM/P5H05FralEVdBDa2xaMNcG/4rlck0utPhS1xahK1fFLg4+ksutOPZ9vguER9agSU93gYQtqeB7LYBNa5/QON322GPWoKmy8eV8yqwrKfGt6HapSJQaWt63Pj7OP8DARTc0+FLLFqERVasPw9KQJtjTgCLtxjdpUiYTlYQsmdECi5IEUo8nmos0WowZVm6pvTfv1xjVqUyUeloetF/PZhNbBy2pfQ0t2W0x0VcGqjV+Uj5JVGyH3o1pVkmAJbcXXs9Hwn8Vm9fuQ1xYTeVWwWTcs3Qn74c7OMj6XzT5UwThggsQUaIRVSYXF2zrQODQ92Ug2vIDNpG1OWmwxkb9XeezdDYn1rBabylWFAIvYmn9wyHujchpsMWGqam5u7uvr00tpj6dPCD/rB1MmyO9MfIfh4X21nTAqLPXCcK6FC6FLvWpmyrjwH/tY+oT4Dwo+WPh4Ozo69NJb+NeGaSsqsH71WT+wdzySsA6duxFRWKnjHhuVS/0Fd3Z2howjnGsHBga0HQrJYG1NT+TSoVBhrJVQGNpvN1rXRv/hHT5fWGcB+nuHxmbsYSMj7NVWcFnVtkJ7eE/QsYOed8E6JZkGqQ/v2lUV/XQDtLNXb87NGiFP8d+cnlStrRDSDeWtN8liIbC2oPplyKhKLQnSHv3QwmI9CRYwy+XEDbsKbUlNkIKqnzptZAO6myYXVapUNKQzODiYWn2d7Bs4J8u0v111tiQN6YAqOP6ojF0BYGX1JG2q1DUIDbZONnb8/YAphLAYAVviB6GJKpigBrV+08TtEBZjqtxqK5sBW02t7WuqzSQsvlUkISwqbUtk2QxRBX1xFTuY80m5hUJVbhUW+oEtuLbsyiRZbgpGQnJabWqwJabQj1cFZT9k/ffzAw4KVbnVWZpMbHWP2l7jlqmFtT2XV4ud+KWcraClybwq6Ou41UH+dsBEpyq3aidTEFuwXDvkfkhFzZtF5ipxYVEhW/5geauC2lFYWgL+zrDbCp2q3Gqe/kVswbXwFPwoCYsZojYZVMiWT1jeqqDv4DbYeTbH5HBRqsqt8gmrvC2YREU2sISdJmD/EjFLQspuyxuWT1XQ/5QffI/72FblVv8Ue94W7JAL+Wuy3Ciks8VMtZDXlgcsf6qgBIgsGBmgHDnmVbk1sSgIbwuOq7rtZC2XWRnGbBFVXDLaEsLyp4pf8D3AGA4NqtxaWcZIaGvA4IIHefFhUS5bPKwAqoKO4VCiyq2hhdeEtoRhEXYMqAi2Fa8stgisAKqCjuHQo8qtraUihbag6XodT2QayX5MqU02pW1BbSD0AKoCj+FQpcqtucVtPWwNm13vHjaTST5Bw2KYZfJQdwo9QPI9wBgObarcWlyO28MW5IogLE7nwiLsJX40YFgMxxYpTfb33QBjOBSqcmt0AwEPW9Bg/cjZXFiEwpuUgCu/h2wr8FjhtzrfYzh0qnJrd8sTb1u3zC6onSfL/8GM6gBhUfbpX/7GcKhV5db0Jk3etuAVH1bDJmHxpYJAYVHeCas+x3BoVuXW+rZy3ragcfePqbC4x/+e0DJOsSdjOD+22FCVimApYWvE7PqgdCoswlrL/lbwlmVREO8xHFTljo2te33agl9yepONrA7/lwJTuZ+wGP4yRh5jOKhKXbCUsAXtTL9jDhcWIRfgLyyGs/AaGcO5984YDqpSIyyFbI1aXPODhcWQl4okYzhkEwpUpV5YCtkShkXY4KnE17ZhISxuKxzDQVVqh6WQLWhNQ85nctjXN1i7cYev5bikLsfNj+GgKm3AUs7WhNUVV2Hhw6KuX4ItD1j8GM6ZPiuq0gws5WyRsHgPFxbn5Rm9d9P0Z8sDFhnDeaXQiKo0Bks5W9Cabzqf5cLi/SnG7V5h0actISx+DCetphtVaQ+WorYMVtd/ubAIHV7uavuC2BLCImM4T2WM9elRlTZhKWoLWt4lG7zZcZvteoZFD1tCWGTXuN26W6hKw7CUttVy0wk73pCw6LF/ndAWDyujib3PzUyZELktAIWqNANLaVsQFj87Zvll3cpeH7b4mveXckekrqVGmyotwVLaFjTYP+L+vVxYzDUWC8IisUVq3pNO3ZgmGMNBVbEAKwK2Woedf+DC4oy9xq1nrd417+8WjQXYSBxVaRVWBGwZba6FlT7CIhS8Z9X1SFpLjVpVmoQVAVvCsAj7cR7glnOGm1Zcyaj4tdRoVqVVWJGxBbelF7jSULhLwSyJh1MNZEGlhn4HqopZWJGxBQmFL45bPLY7ELkJAM2qtA0rMragzeQGm3/ZtiTTiKpiHFZkbJGpGXyHklRUFfuwImDLY/OfwKEQVcUOLKVtkc1/YNMy6IH3w0FVsQYrAvctsuUa3quog6W0rcCwUFUsw1LUVgBYqCr2YSlnyx8sVEULLIVs+YSFquiCpYQtb1ioikZYstvygIWq6IUlry0hLFRFOywZbfGwUBXCktMWgYWqEJbMtvq4hqoQlpy24NpmrgUe1UFVNMIK01Yn11AVwpLZVtBBaFRFNayQbYUAi1pVlMIKzZZUWDSrohdWCLYkwaJcFdWwpNoSDwtV0Q5Lki2RsFAVwpJmSwwsVIWwJNtStOYdYdFrS7mad4RFtS2Fat4RFu22lKh5R1hoS/6ad4SFtnzDQlUISwZbMta8Iyy0JX/NO8LC9itbstS8IyxsnrbCr3lHWNh82Aqz5h1hYfNti9S8oyqEJb8tVCW+/R++masNzLdd0QAAAABJRU5ErkJggg==", + "description": "Displays latest values of the attributes or timeseries data for multiple entities in a radar chart. Supports numeric values only.", + "descriptor": { + "type": "latest", + "sizeX": 7, + "sizeY": 5, + "resources": [ + { + "url": "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js" + } + ], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \n var barData = {\n labels: [],\n datasets: []\n };\n\n var backgroundColor = tinycolor(self.ctx.data[0].dataKey.color);\n backgroundColor.setAlpha(0.2);\n var borderColor = tinycolor(self.ctx.data[0].dataKey.color);\n borderColor.setAlpha(1);\n var dataset = {\n label: self.ctx.datasources[0].name,\n data: [],\n backgroundColor: backgroundColor.toRgbString(),\n borderColor: borderColor.toRgbString(),\n pointBackgroundColor: borderColor.toRgbString(),\n pointBorderColor: borderColor.darken().toRgbString(),\n borderWidth: 1\n }\n \n barData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n barData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n }\n \n var floatingPoint;\n if (typeof self.ctx.decimals !== 'undefined' && self.ctx.decimals !== null) {\n floatingPoint = self.ctx.widget.config.decimals;\n } else {\n floatingPoint = 2;\n }\n\n var ctx = $('#radarChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'radar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false,\n scale: {\n ticks: {\n callback: function(tick) {\n \treturn tick.toFixed(floatingPoint);\n }\n }\n }\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n } \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n self.ctx.chart.resize();\n }\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-chart-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Radar\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/radial_gauge.json b/application/src/main/data/json/system/widget_types/radial_gauge.json new file mode 100644 index 0000000000..d0111e2866 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/radial_gauge.json @@ -0,0 +1,21 @@ +{ + "fqn": "analogue_gauges.radial_gauge_canvas_gauges", + "name": "Radial gauge", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC+lBMVEX////m3d398OrwlGL1vJ33ybH75Njthk7xoXX51sTzr4n//v7mUQDx8fHn5+dsXFzt7e3+/f3h4eHj4+Pl5eX7+vr09PTp6enq6urv7+/9/Pz8+/vy8vL49/fd3d2lpaX5+fn29vbk5OT++fff39+9vb20nZCxsbGzloapoaHb29r++/ns7OyampqenZ18d3awgmq0tLT++PX+9vL97ubExMSqqqqVlZS3sa/79fL31cO6urqKiYnY19f74tT62snwuZvOgVehoaGwpqDGmoGpjHz98uzi2trNxsPy0cCZgnWphnP86+Hc1NPIyMi3traurq6uiHPokmPZglTnVQbMzMzaycDzxq6srKysnZW5oJPxrojtrIh+fX2DfHnp39rU1NT63s/a0M/TzMnBwcHIwL/Bs633xqzKs6esp6XntJiblpOek42WjovoqoefjYOXe2zHiGepemH86N324NXx3dPNvbW5rKaFgoGwj33fkGXnhVD37ejq1szpyrrFvrqPj4+umY6klo7toHXonnV0b29TTk4uKirs5N/gzcKvqqinp6fGsKX2waS0qaPWtKGyopnBppe5monAloCOgoCeiHzSlnSngGu3hWjYjWWufGBGQEDpaSTpYhnz6ePi2NLl0cbtzb34zrjfxLW1r63nvqmnn5/Qr568qZ7JqprCn4yXiYJ5amqzfF68flzCf1rIgFj38e326+Xm3NbWz8/Vxbzavq/RuazOtqnCraKgmpjXq5PQp5Djp4awlYbXooSrkoTOnYLzqYC7lH/YmHXNlHTGkXS6jHOzinOig3LwmGfvj1vtgUbrcC3x5t/v2s/Ozs75076+uba8tLLuw6ymm5SSh4HypHnemnW+jnS/h2jRjGXuiVLhhFHsdzjoXA/RwbjduKT1tpS9nozcpIWYhnuBeHOPe3GbeGVbVlbQ0ND51sPkx7fTu671uJfcrpX0tJHJo46nj4Pmo32Nf3fjnHXxn3FNSEjOysjFuLKjeWNnYWHsfUE8NzepajCWAAAUc0lEQVR42tSWW2xLcRzH/6GH/M/Rk3Pac3p6tV7mEh0JiqyChU6CxSWR1WwuQ6KWWh9clzBpvbiUbWqvmGVZ6dOIy7LNhjdEIsyTiMs8uYWIROLB75z2nNPt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", + "description": "Preconfigured gauge to display any value reading. Allows to configure value range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 6, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-analogue-radial-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < -100) {\\n\\tvalue = -100;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":10,\"highlights\":[],\"showUnitTitle\":true,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":10,\"valueInt\":3,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":36,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"minValue\":-100,\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/raspberry_pi_gpio_control.json b/application/src/main/data/json/system/widget_types/raspberry_pi_gpio_control.json new file mode 100644 index 0000000000..6a9b31d473 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/raspberry_pi_gpio_control.json @@ -0,0 +1,21 @@ +{ + "fqn": "gpio_widgets.raspberry_pi_gpio_control", + "name": "Raspberry Pi GPIO Control", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to display state of the GPIO for target Raspberry Pi device using latest attribute values. You should set the label of the selected data key to GPIO pin number (e.g. '1') and use boolean values for widget to display the data.", + "descriptor": { + "type": "rpc", + "sizeX": 6, + "sizeY": 10.5, + "resources": [], + "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n {{rpcErrorText}}\n \n
", + "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel mat-slide-toggle {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel.col-0 mat-slide-toggle {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 mat-slide-toggle {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", + "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-control-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n \n var i, gpio;\n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n scope.gpioList = [];\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false\n }\n );\n }\n\n scope.requestTimeout = settings.requestTimeout || 1000;\n\n scope.switchPanelBackgroundColor = settings.switchPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioStatusRequest = {\n method: \"getGpioStatus\",\n paramsBody: \"{}\"\n };\n \n if (settings.gpioStatusRequest) {\n scope.gpioStatusRequest.method = settings.gpioStatusRequest.method || scope.gpioStatusRequest.method;\n scope.gpioStatusRequest.paramsBody = settings.gpioStatusRequest.paramsBody || scope.gpioStatusRequest.paramsBody;\n }\n \n scope.gpioStatusChangeRequest = {\n method: \"setGpioStatus\",\n paramsBody: \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n };\n \n if (settings.gpioStatusChangeRequest) {\n scope.gpioStatusChangeRequest.method = settings.gpioStatusChangeRequest.method || scope.gpioStatusChangeRequest.method;\n scope.gpioStatusChangeRequest.paramsBody = settings.gpioStatusChangeRequest.paramsBody || scope.gpioStatusChangeRequest.paramsBody;\n }\n \n scope.parseGpioStatusFunction = \"return body[pin] === true;\";\n \n if (settings.parseGpioStatusFunction && settings.parseGpioStatusFunction.length > 0) {\n scope.parseGpioStatusFunction = settings.parseGpioStatusFunction;\n }\n \n scope.parseGpioStatusFunction = new Function(\"body, pin\", scope.parseGpioStatusFunction);\n \n function requestGpioStatus() {\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusRequest.method, \n scope.gpioStatusRequest.paramsBody, \n scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n for (var g = 0; g < scope.gpioList.length; g++) {\n var gpio = scope.gpioList[g];\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled; \n self.ctx.detectChanges();\n }\n }\n );\n }\n \n function changeGpioStatus(gpio) {\n var pin = gpio.pin + '';\n var enabled = !gpio.enabled;\n enabled = enabled === true ? 'true' : 'false';\n var paramsBody = scope.gpioStatusChangeRequest.paramsBody;\n var requestBody = JSON.parse(paramsBody.replace(\"\\\"{$pin}\\\"\", pin).replace(\"\\\"{$enabled}\\\"\", enabled));\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusChangeRequest.method, \n requestBody, scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled;\n self.ctx.detectChanges();\n }\n );\n }\n \n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n scope.gpioClick = function($event, gpio) {\n if (scope.rpcEnabled && !scope.executingRpcRequest) {\n changeGpioStatus(gpio);\n }\n };\n \n scope.gpioToggleChange = function($event, gpio) {\n gpio.enabled = !$event.checked;\n $event.source.toggle();\n self.ctx.detectChanges();\n }\n \n if (scope.rpcEnabled) {\n requestGpioStatus(); \n }\n \n self.onResize();\n}\n\nself.onResize = function() {\n var scope = self.ctx.$scope;\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.prefferedRowHeight = prefferedRowHeight;\n var ratio = prefferedRowHeight/32;\n \n var css = '.mat-slide-toggle .mat-slide-toggle-bar {\\n' +\n ' height: ' + 14*ratio+'px;\\n'+\n ' width: ' + 36*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb-container {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-ripple {\\n' +\n ' height: ' + 40*ratio+'px;\\n'+\n ' width: ' + 40*ratio+'px;\\n'+\n ' top: calc(50% - '+20*ratio+'px);\\n'+\n ' left: calc(50% - '+20*ratio+'px);\\n'+\n '}\\n';\n css += '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n\n cssParser.createStyleElement(namespace, css);\n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gpio-control-widget-settings", + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#008a00\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":7,\"label\":\"GPIO 4 (GPCLK0)\",\"row\":3,\"col\":0,\"_uniqueKey\":0},{\"pin\":11,\"label\":\"GPIO 17\",\"row\":5,\"col\":0,\"_uniqueKey\":1},{\"pin\":12,\"label\":\"GPIO 18\",\"row\":5,\"col\":1,\"_uniqueKey\":2},{\"_uniqueKey\":3,\"pin\":13,\"label\":\"GPIO 27\",\"row\":6,\"col\":0},{\"_uniqueKey\":4,\"pin\":15,\"label\":\"GPIO 22\",\"row\":7,\"col\":0},{\"_uniqueKey\":5,\"pin\":16,\"label\":\"GPIO 23\",\"row\":7,\"col\":1},{\"_uniqueKey\":6,\"pin\":18,\"label\":\"GPIO 24\",\"row\":8,\"col\":1},{\"_uniqueKey\":7,\"pin\":22,\"label\":\"GPIO 25\",\"row\":10,\"col\":1},{\"_uniqueKey\":8,\"pin\":29,\"label\":\"GPIO 5\",\"row\":14,\"col\":0},{\"_uniqueKey\":9,\"pin\":31,\"label\":\"GPIO 6\",\"row\":15,\"col\":0},{\"_uniqueKey\":10,\"pin\":32,\"label\":\"GPIO 12\",\"row\":15,\"col\":1},{\"_uniqueKey\":11,\"pin\":33,\"label\":\"GPIO 13\",\"row\":16,\"col\":0},{\"_uniqueKey\":12,\"pin\":35,\"label\":\"GPIO 19\",\"row\":17,\"col\":0},{\"_uniqueKey\":13,\"pin\":36,\"label\":\"GPIO 16\",\"row\":17,\"col\":1},{\"_uniqueKey\":14,\"pin\":37,\"label\":\"GPIO 26\",\"row\":18,\"col\":0},{\"_uniqueKey\":15,\"pin\":38,\"label\":\"GPIO 20\",\"row\":18,\"col\":1},{\"_uniqueKey\":16,\"pin\":40,\"label\":\"GPIO 21\",\"row\":19,\"col\":1}]},\"title\":\"Raspberry Pi GPIO Control\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/raspberry_pi_gpio_panel.json b/application/src/main/data/json/system/widget_types/raspberry_pi_gpio_panel.json new file mode 100644 index 0000000000..b6417d7be3 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/raspberry_pi_gpio_panel.json @@ -0,0 +1,21 @@ +{ + "fqn": "gpio_widgets.raspberry_pi_gpio_panel", + "name": "Raspberry Pi GPIO Panel", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to change state of the GPIO for Raspberry Pi device using RPC commands. Requires handling of the RPC commands in the device firmware. Uses 'getGpioStatus' and 'setGpioStatus' RPC calls", + "descriptor": { + "type": "latest", + "sizeX": 7, + "sizeY": 10.5, + "resources": [], + "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n
", + "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", + "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-panel-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.detectChanges();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var css = '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n \n cssParser.createStyleElement(namespace, css); \n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gpio-panel-widget-settings", + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"gpioList\":[{\"pin\":1,\"label\":\"3.3V\",\"row\":0,\"col\":0,\"color\":\"#fc9700\",\"_uniqueKey\":0},{\"pin\":2,\"label\":\"5V\",\"row\":0,\"col\":1,\"color\":\"#fb0000\",\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 2 (I2C1_SDA)\",\"row\":1,\"col\":0,\"color\":\"#02fefb\",\"_uniqueKey\":2},{\"color\":\"#fb0000\",\"pin\":4,\"label\":\"5V\",\"row\":1,\"col\":1},{\"color\":\"#02fefb\",\"pin\":5,\"label\":\"GPIO 3 (I2C1_SCL)\",\"row\":2,\"col\":0},{\"color\":\"#000000\",\"pin\":6,\"label\":\"GND\",\"row\":2,\"col\":1},{\"color\":\"#00fd00\",\"pin\":7,\"label\":\"GPIO 4 (GPCLK0)\",\"row\":3,\"col\":0},{\"color\":\"#fdfb00\",\"pin\":8,\"label\":\"GPIO 14 (UART_TXD)\",\"row\":3,\"col\":1},{\"color\":\"#000000\",\"pin\":9,\"label\":\"GND\",\"row\":4,\"col\":0},{\"color\":\"#fdfb00\",\"pin\":10,\"label\":\"GPIO 15 (UART_RXD)\",\"row\":4,\"col\":1},{\"color\":\"#00fd00\",\"pin\":11,\"label\":\"GPIO 17\",\"row\":5,\"col\":0},{\"color\":\"#00fd00\",\"pin\":12,\"label\":\"GPIO 18\",\"row\":5,\"col\":1},{\"color\":\"#00fd00\",\"pin\":13,\"label\":\"GPIO 27\",\"row\":6,\"col\":0},{\"color\":\"#000000\",\"pin\":14,\"label\":\"GND\",\"row\":6,\"col\":1},{\"color\":\"#00fd00\",\"pin\":15,\"label\":\"GPIO 22\",\"row\":7,\"col\":0},{\"color\":\"#00fd00\",\"pin\":16,\"label\":\"GPIO 23\",\"row\":7,\"col\":1},{\"color\":\"#fc9700\",\"pin\":17,\"label\":\"3.3V\",\"row\":8,\"col\":0},{\"color\":\"#00fd00\",\"pin\":18,\"label\":\"GPIO 24\",\"row\":8,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":19,\"label\":\"GPIO 10 (SPI_MOSI)\",\"row\":9,\"col\":0},{\"color\":\"#000000\",\"pin\":20,\"label\":\"GND\",\"row\":9,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":21,\"label\":\"GPIO 9 (SPI_MISO)\",\"row\":10,\"col\":0},{\"color\":\"#00fd00\",\"pin\":22,\"label\":\"GPIO 25\",\"row\":10,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":23,\"label\":\"GPIO 11 (SPI_SCLK)\",\"row\":11,\"col\":0},{\"color\":\"#fd01fd\",\"pin\":24,\"label\":\"GPIO 8 (SPI_CE0)\",\"row\":11,\"col\":1},{\"color\":\"#000000\",\"pin\":25,\"label\":\"GND\",\"row\":12,\"col\":0},{\"color\":\"#fd01fd\",\"pin\":26,\"label\":\"GPIO 7 (SPI_CE1)\",\"row\":12,\"col\":1},{\"color\":\"#ffffff\",\"pin\":27,\"label\":\"ID_SD\",\"row\":13,\"col\":0},{\"color\":\"#ffffff\",\"pin\":28,\"label\":\"ID_SC\",\"row\":13,\"col\":1},{\"color\":\"#00fd00\",\"pin\":29,\"label\":\"GPIO 5\",\"row\":14,\"col\":0},{\"color\":\"#000000\",\"pin\":30,\"label\":\"GND\",\"row\":14,\"col\":1},{\"color\":\"#00fd00\",\"pin\":31,\"label\":\"GPIO 6\",\"row\":15,\"col\":0},{\"color\":\"#00fd00\",\"pin\":32,\"label\":\"GPIO 12\",\"row\":15,\"col\":1},{\"color\":\"#00fd00\",\"pin\":33,\"label\":\"GPIO 13\",\"row\":16,\"col\":0},{\"color\":\"#000000\",\"pin\":34,\"label\":\"GND\",\"row\":16,\"col\":1},{\"color\":\"#00fd00\",\"pin\":35,\"label\":\"GPIO 19\",\"row\":17,\"col\":0},{\"color\":\"#00fd00\",\"pin\":36,\"label\":\"GPIO 16\",\"row\":17,\"col\":1},{\"color\":\"#00fd00\",\"pin\":37,\"label\":\"GPIO 26\",\"row\":18,\"col\":0},{\"color\":\"#00fd00\",\"pin\":38,\"label\":\"GPIO 20\",\"row\":18,\"col\":1},{\"color\":\"#000000\",\"pin\":39,\"label\":\"GND\",\"row\":19,\"col\":0},{\"color\":\"#00fd00\",\"pin\":40,\"label\":\"GPIO 21\",\"row\":19,\"col\":1}],\"ledPanelBackgroundColor\":\"#008a00\"},\"title\":\"Raspberry Pi GPIO Panel\",\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"7\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.22518255793320163,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"11\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7008206860666621,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"12\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.42600325102193426,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"13\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.48362241571415243,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"29\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7217670147518815,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/round_switch.json b/application/src/main/data/json/system/widget_types/round_switch.json new file mode 100644 index 0000000000..1e7b63ea18 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/round_switch.json @@ -0,0 +1,21 @@ +{ + "fqn": "control_widgets.round_switch", + "name": "Round switch", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to send the RPC call to device when user toggle the switch. Advanced widget settings allow you to configure how to fetch the initial value of the switch.", + "descriptor": { + "type": "rpc", + "sizeX": 2.5, + "sizeY": 2, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-round-switch-widget-settings", + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":false,\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\",\"title\":\"Round switch\",\"retrieveValueMethod\":\"rpc\",\"valueKey\":\"value\",\"parseValueFunction\":\"return data ? true : false;\",\"convertValueFunction\":\"return value;\"},\"title\":\"Round switch\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/route_map___google.json b/application/src/main/data/json/system/widget_types/route_map___google.json new file mode 100644 index 0000000000..f5e62582d2 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/route_map___google.json @@ -0,0 +1,21 @@ +{ + "fqn": "maps_v2.route_map", + "name": "Route Map - Google", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Trip animation on the Google maps. Allows to visualize location change over time. Use Trip Animation widget for advanced features.", + "descriptor": { + "type": "timeseries", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], + "templateHtml": "", + "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-route-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"google-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d2\",\"useColorFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - Google\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/route_map___openstreet.json b/application/src/main/data/json/system/widget_types/route_map___openstreet.json new file mode 100644 index 0000000000..173807b9bc --- /dev/null +++ b/application/src/main/data/json/system/widget_types/route_map___openstreet.json @@ -0,0 +1,21 @@ +{ + "fqn": "maps_v2.route_map_openstreetmap", + "name": "Route Map - OpenStreet", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Trip animation on the OpenStreetMap. Allows to visualize location change over time. Use Trip Animation widget for advanced features.", + "descriptor": { + "type": "timeseries", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-route-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"openstreet-map\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d3\",\"useColorFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - OpenStreet\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/route_map___tencent.json b/application/src/main/data/json/system/widget_types/route_map___tencent.json new file mode 100644 index 0000000000..8ee658089e --- /dev/null +++ b/application/src/main/data/json/system/widget_types/route_map___tencent.json @@ -0,0 +1,21 @@ +{ + "fqn": "maps_v2.route_map_tencent_maps", + "name": "Route Map - Tencent", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Trip animation on the Tencent maps. Allows to visualize location change over time. Use Trip Animation widget for advanced features.", + "descriptor": { + "type": "timeseries", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], + "templateHtml": "", + "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-route-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"tencent-map\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details
\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d3\",\"useColorFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - Tencent\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/rpc_button.json b/application/src/main/data/json/system/widget_types/rpc_button.json new file mode 100644 index 0000000000..4860f6a9f0 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/rpc_button.json @@ -0,0 +1,21 @@ +{ + "fqn": "control_widgets.rpcbutton", + "name": "RPC Button", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAN8SURBVHja7dvbSxRRHAfw/X9+bhcvrJZSmllhZFSCWBDaY4FUhBRZCQXR7akLyoJYlqxU0g0tukF0oYQM3bxsYVRgbuuyu7k7szNn9vSb2Wx9qB56ac72/T7MHGYZmA/nMjvDbzzSTkbpOASPw7CUjkPx2AwhTNNQNKYphE1hiCXMtK5pWkrB8GXraZMl0sMOQ9dEdqApmIzQdIMlDDH1lLIMh5LSTYZYIp1MS6XDAGF5uEPiltoQK85dwhAtKhVPVLMhRj5ADAcyqzpkNgtJqQ9JAQIIIIAAAggggAACCCB/8wog8btfAjd+f9Y3t0GmW5bS8rO/fvavaXB2jWVlZbVHwjLC+7L1x2P8rN1ZQcWtX90EyWws7r7bSuf+BKnz+f1t3norTPV+/0HvNiFP0+5b54sahYsgn+mQrdksZfL6hYcZGe2betoxyG/LtP7OiXlIJW/a6F2Y9nPjKI3MLtrBjbM05CJIsqT6tdOI1BbWFuyRQVpbupw7yGygqqLCBZCTNJqFdNGTAQpwY2bwo5vmyL1iqr+uS3l4yaTsoaEg7bT0VXWyn/xyquQHpDz4JuCrFWHaG48OrSuJXqLHLly1Yl01VDkuq6r7+i5Qd5ABsqlctlMiN0eI0/BZhu09rXggu10J4TVoYMlWWeKr51x2IM3lcp83k4NUfAqVbrQYsn3w/lvuvTt0lQ9Hn027CNJLt3m7YaXctIaXYCHnIWfog5Src3Okg/pldo7Ys8O7S9qHXrgIEllWee35uYJ9MkAHXgVq3s9Dxguan56iHCRZUa3/hPDAa3/U7dtiuGloTTZ7aXFrnO9xPio8Zs5D5JViaqpbsGpdpK4cxDxRRItbpl02R1Jfsje2zIy+8LCI/ekkc0a4b7Lj3y8ggAACCCCAAAIIIIAA8v9C8qaoRosqXngmsmVOXHgWVxsSyxaemXosJFR2mBMxpxRQpOPB0YihKsOIjAbjdnEml8vOhUaGX97s7VEwvTdfDo+E5kyn7pe7JDQWVDZjIbtDMk5JuZGcSyQScQXDlz2XNJyS8vwp8s+bzy6kzJsPYfIigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg/yTfATinNXQTM7TQAAAAAElFTkSuQmCC", + "description": "Allows to send RPC command when user press the button.", + "descriptor": { + "type": "rpc", + "sizeX": 4, + "sizeY": 2, + "resources": [], + "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n
\n
\n
\n {{ error }}\n
\n
", + "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", + "controllerScript": "var requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n \n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n if (rpcParams.length) {\n try {\n rpcParams = JSON.parse(rpcParams);\n } catch (e) {}\n }\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n }\n commandPromise.subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n );\n };\n}\n\nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-send-rpc-widget-settings", + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":5000,\"oneWayElseTwoWay\":true,\"buttonText\":\"Send RPC\",\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"methodName\":\"rpcCommand\",\"methodParams\":\"{}\"},\"title\":\"RPC Button\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json b/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json new file mode 100644 index 0000000000..ac995e24cd --- /dev/null +++ b/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json @@ -0,0 +1,21 @@ +{ + "fqn": "control_widgets.rpc_debug_terminal", + "name": "RPC debug terminal", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAWcklEQVR42u2dB5QWRZeGi6QgklFUDJhFMCJGRFHMomMAMStiBvUXRTFgRjGLERMoiIBZ1GVXdNcF4yIe5agYdvVnfkAwIAoiIs4+/72na3u+NN8wAzsD73vmzOnur7rCrbfuvV3VfSuUlZXNnDnzmGOOadKkSRCEKgAKlZSUzJgxA1IFWNWyZUsJRagutGjRAlIFdJVkIVQvevbsGWQBhWpH06ZNJQRBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEITlRK9evc5KwAdAnTt3rlevXvz1oIMOir+efvrp3bp1a9WqVUYOderUOfDAAy+77LJLL7308MMPX2ONNYop99577+ULyaOPPrpwsvfff59kG264Ya0W8uDBg6+55pqq5PDRRx8hh3XXXbfWtPnzzz8vK4/PPvtsjz328F9ffvnljF9///33q6++Ot6+/vrrv/fee+kEX3zxxeabb15biHXqqafecsstK5S4a6655pIlS/7444+11lprtSPWEUcc0alTp/333//hhx/mdNasWa54nFj9+vXj19122+2MM8747rvvuHLiiSe6rnrrrbc4feyxxzp27Aifbr/9dk6nTp1aW4j10ksvkf8uu+yyQoVM/piC1VFjbbbZZtGu+RUXhBMLgxjTQymuvPrqqxzvs88+HH/44Ydps/jJJ59wccstt8wua+ONN7722mvvuOOOQw89NJtYlAIv7777buwp+aSJtd122w0YMIBbUDDRUjMYsL8bbbSRn+63336ctm/f3k/XXnvtiy66iFsw4htssAE/MWzSlWncuDEXUc/kT7L4cXn9+vVxD+65557bbrttr732iunJ7dxzz91mm22o4QEHHECIA27v0aMHgkKFDxs2jIsk23PPPWng0KFD995773jv3wx+3Ldv34svvrh58+a06L777jv77LMbNGiQtgBcf+CBB8hz6623XnWIBd59990CxOrSpQtX3n77bY6HDBnCMfJNZ4gfRl9uuummGQWhz3788UfS//nnn/z/4Ycf0sR6/PHHOcVkLF26lAMkmybW3LlzMSVuaseOHes/jRkzhlPI7aeuLI8//niOGzVq5PxetmzZX3/9VVpayvFdd92Vrs86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", + "description": "Allows to send any RPC command using its name and parameters to device. Useful for debug.", + "descriptor": { + "type": "rpc", + "sizeX": 9.5, + "sizeY": 5.5, + "resources": [], + "templateHtml": "
", + "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n", + "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-rpc-terminal-widget-settings", + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"RPC debug terminal\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/rpc_remote_shell.json b/application/src/main/data/json/system/widget_types/rpc_remote_shell.json new file mode 100644 index 0000000000..f1a486b8a2 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/rpc_remote_shell.json @@ -0,0 +1,21 @@ +{ + "fqn": "control_widgets.rpc_remote_shell", + "name": "RPC remote shell", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAU6klEQVR42u2dCbSW0xrHtyZDOpVkyFCKIplCZpLMIVJIhsgYmSUakLniInOGyFRXaCn3ilws89Q1RrpRSq4k15Tx3N96Ht9e73m/4Xzn9EVH//8666z97u99997v3v/9PM8e3meH8vLyWbNmdevWrUGDBkEQFgNQqGvXrtOmTYNUAVatssoqqhShVGjcuDGkCsgq1YVQWnTv3j1IAwolR1lZmSpBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEAShmujRo8fxGfAB0NZbb127du3465577hl/7d2796677tqkSZNUCsstt9wee+zRv3//c845p0uXLvXq1ftLVtQ222zDJ50PP/xwlZ7abbfdeOruu+8mXKtWLcKzZ89eJoj1/vvvl1fEe++9t9122/mvEyZMSP26aNGiQYMGxcfXXHPNl156KXnDBx980KpVq6Xh1aD7FVdcsdVWW4lYfxqx9t9//y233JJauPXWW7mcM2eOCx4n1imnnMKv1Oyxxx47b948Yg4//HCXVc888wyXt99+e7t27eDT8OHDuXzttdeWhlcbOnQohTnmmGNErD+NWC1btox6zWPQiZFYKMR4P5QiZuLEiYR32WUXwq+//npSLb711ltEbrDBBqmMDjnkENTluuuue+mllx533HEe2axZs/POO++mm24666yzVl11VY9s27Ytd3bo0GHvvfe+/PLLhw0btummmxJ/0EEHXXfddRdddFGbNm2SKcPpCy+8kET69evXqFEjj6QzPPXUU5Rk3LhxJ554Yry5c+fOJHjttdfisIDS5qyTpk2bnn322eR1/vnnR+kbiUV2Q4YMufLKK7fffvvkU+uss86AAQMoxhlnnBEdIIhYLWPMiy++WIBYO+64IzHPP/884csuu4wwJEgmiB2GQbbeeuulMpo8eTI3z5w5k/8kS8xOO+20cOFCLr/77jv+z507t3nz5sT37NnTpSb/f/75Z/5///33Dz74IIGffvrJ74/pQ5pffvmFyB9++MGbbf311yceLxcew81Tp071m2l1T8TTueOOO7IrZOONN/7qq6/IlxTQ+9y57777RmKRPsl6qX799VdeNhLom2++ie+CH4S11lpLxPqd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", + "description": "Allows to emulate remote shell. Requires custom implementation on the target device to work properly.", + "descriptor": { + "type": "rpc", + "sizeX": 9.5, + "sizeY": 5.5, + "resources": [], + "templateHtml": "
", + "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n", + "controllerScript": "var requestTimeout = 500;\nvar commandStatusPollingInterval = 200;\n\nvar welcome = 'Welcome to ThingsBoard RPC remote shell.\\n';\n\nvar terminal, rpcEnabled, simulated, deviceName, cwd;\nvar commandExecuting = false;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n rpcEnabled = subscription.rpcEnabled;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n } else {\n deviceName = 'Simulated';\n simulated = true;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n \n terminal = $('#device-terminal', self.ctx.$container).terminal(\n function (command) {\n if (command && command.trim().length) {\n try {\n if (simulated) {\n this.echo(command);\n } else {\n sendCommand(this, command);\n }\n } catch(e) {\n this.error(e + '');\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: false,\n enabled: rpcEnabled,\n prompt: rpcEnabled ? currentPrompt : '',\n name: 'shell',\n pauseEvents: false,\n keydown: function (e, term) {\n if ((e.which == 67 || e.which == 68) && e.ctrlKey) { // CTRL+C || CTRL+D\n if (commandExecuting) {\n terminateCommand(term);\n return false;\n }\n }\n },\n onInit: initTerm\n }\n );\n \n};\n\nfunction initTerm(terminal) {\n terminal.echo(welcome);\n if (!rpcEnabled) {\n terminal.error('Target device is not set!\\n');\n } else {\n terminal.echo('Current target device for RPC terminal: [[b;#fff;]' + deviceName + ']\\n');\n if (!simulated) {\n terminal.pause();\n getTermInfo(terminal,\n function (remoteTermInfo) {\n if (remoteTermInfo) {\n terminal.echo('Remote platform info:');\n terminal.echo('OS: [[b;#fff;]' + remoteTermInfo.platform + ']');\n if (remoteTermInfo.release) {\n terminal.echo('OS release: [[b;#fff;]' + remoteTermInfo.release + ']');\n }\n terminal.echo('\\r');\n } else {\n terminal.echo('[[;#f00;]Unable to get remote platform info.\\nDevice is not responding.]\\n');\n }\n terminal.resume();\n });\n }\n }\n}\n\nfunction currentPrompt(callback) {\n if (cwd) {\n callback('[[b;#2196f3;]' + deviceName +']: [[b;#8bc34a;]' + cwd +']> ');\n } else {\n callback('[[b;#8bc34a;]' + deviceName +']> ');\n }\n}\n\nfunction getTermInfo(terminal, callback) {\n self.ctx.controlApi.sendTwoWayCommand('getTermInfo', null, requestTimeout).subscribe(\n function (termInfo) {\n cwd = termInfo.cwd;\n if (callback) {\n callback(termInfo);\n } \n },\n function () {\n if (callback) {\n callback(null);\n }\n }\n );\n}\n\nfunction sendCommand(terminal, command) {\n terminal.pause();\n var sendCommandRequest = {\n command: command,\n cwd: cwd\n };\n self.ctx.controlApi.sendTwoWayCommand('sendCommand', sendCommandRequest, requestTimeout).subscribe(\n function (responseBody) {\n if (responseBody && responseBody.ok) {\n commandExecuting = true;\n setTimeout( pollCommandStatus.bind(null,terminal), commandStatusPollingInterval );\n } else {\n var error = responseBody ? responseBody.error : 'Unhandled error.';\n terminal.error(error);\n terminal.resume();\n }\n },\n function () {\n onRpcError(terminal);\n }\n );\n}\n\nfunction terminateCommand(terminal) {\n self.ctx.controlApi.sendTwoWayCommand('terminateCommand', null, requestTimeout).subscribe(\n function (responseBody) {\n if (!responseBody.ok) {\n commandExecuting = false;\n terminal.error(responseBody.error);\n terminal.resume();\n } \n },\n function () {\n onRpcError(terminal);\n }\n ); \n}\n\nfunction onRpcError(terminal) {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.resume();\n}\n\nfunction pollCommandStatus(terminal) {\n self.ctx.controlApi.sendTwoWayCommand('getCommandStatus', null, requestTimeout).subscribe(\n function (commandStatusResponse) {\n for (var i=0;i 0) {\n var tbDatasource = self.ctx.datasources[0];\n var datasourceId = 'tbDatasource' + 0;\n self.ctx.$container.append(\n \"
\"\n );\n \n self.ctx.datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n \n var tableId = 'table' + 0;\n self.ctx.datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n if (self.ctx.labelPosition === 'top') {\n table.css('text-align', 'left');\n }\n \n if (tbDatasource.dataKeys.length > 0) {\n var dataKey = tbDatasource.dataKeys[0];\n var labelCellId = 'labelCell' + 0;\n var cellId = 'cell' + 0;\n if (self.ctx.labelPosition === 'left') {\n table.append(\n \"\" +\n dataKey.label +\n \"\");\n } else {\n table.append(\n \"\" +\n dataKey.label +\n \"\");\n }\n self.ctx.labelCell = $('#' + labelCellId, table);\n self.ctx.valueCell = $('#' + cellId, table);\n self.ctx.valueCell.html(0 + ' ' + self.ctx.units);\n }\n }\n \n $.fn.textWidth = function(){\n var html_org = $(this).html();\n var html_calc = '' + html_org + '';\n $(this).html(html_calc);\n var width = $(this).find('span:first').width();\n $(this).html(html_org);\n return width;\n }; \n \n self.onResize();\n};\n\nself.onDataUpdated = function() {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n\n if (self.ctx.valueCell && self.ctx.data.length > 0) {\n var cellData = self.ctx.data[0];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var txtValue;\n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (self.ctx.datasources.length > 0 && self.ctx.datasources[0].dataKeys.length > 0) {\n dataKey = self.ctx.datasources[0].dataKeys[0];\n if (dataKey.decimals || dataKey.decimals === 0) {\n decimals = dataKey.decimals;\n }\n if (dataKey.units) {\n units = dataKey.units;\n }\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCell.html(txtValue);\n var targetWidth;\n var minDelta;\n if (self.ctx.labelPosition === 'left') {\n targetWidth = self.ctx.datasourceContainer.width() - self.ctx.labelCell.width();\n minDelta = self.ctx.width/16 + self.ctx.padding;\n } else {\n targetWidth = self.ctx.datasourceContainer.width();\n minDelta = self.ctx.padding;\n }\n var delta = targetWidth - self.ctx.valueCell.textWidth();\n var fontSize = self.ctx.valueFontSize;\n if (targetWidth > minDelta) {\n while (delta < minDelta && fontSize > 6) {\n fontSize--;\n self.ctx.valueCell.css('font-size', fontSize+'px');\n delta = targetWidth - self.ctx.valueCell.textWidth();\n }\n }\n }\n } \n \n};\n\nself.onResize = function() {\n var labelFontSize;\n if (self.ctx.labelPosition === 'top') {\n self.ctx.padding = self.ctx.height/20;\n labelFontSize = self.ctx.height/4;\n self.ctx.valueFontSize = self.ctx.height/2;\n } else {\n self.ctx.padding = self.ctx.width/50;\n labelFontSize = self.ctx.height/2.5;\n self.ctx.valueFontSize = self.ctx.height/2;\n if (self.ctx.width/self.ctx.height <= 2.7) {\n labelFontSize = self.ctx.width/7;\n self.ctx.valueFontSize = self.ctx.width/6;\n }\n }\n self.ctx.padding = Math.min(12, self.ctx.padding);\n \n if (self.ctx.labelCell) {\n self.ctx.labelCell.css('font-size', labelFontSize+'px');\n self.ctx.labelCell.css('padding', self.ctx.padding+'px');\n }\n if (self.ctx.valueCell) {\n self.ctx.valueCell.css('font-size', self.ctx.valueFontSize+'px');\n self.ctx.valueCell.css('padding', self.ctx.padding+'px');\n } \n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n};\n\n\nself.onDestroy = function() {\n};\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-simple-card-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-simple-card-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/simple_gauge.json b/application/src/main/data/json/system/widget_types/simple_gauge.json new file mode 100644 index 0000000000..3ec1196d41 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/simple_gauge.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.simple_gauge_justgage", + "name": "Simple gauge", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display any value reading as a circle. Allows to configure value range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 2, + "sizeY": 2, + "resources": [], + "templateHtml": "\n", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "\nself.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#ef6c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"gaugeColor\":\"#eeeeee\",\"gaugeType\":\"donut\"},\"title\":\"Simple gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/simple_neon_gauge.json b/application/src/main/data/json/system/widget_types/simple_neon_gauge.json new file mode 100644 index 0000000000..0028a0d7fd --- /dev/null +++ b/application/src/main/data/json/system/widget_types/simple_neon_gauge.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.simple_neon_gauge_justgage", + "name": "Simple neon gauge", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display any value reading as a doughnut. Allows to configure value range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 3, + "sizeY": 3, + "resources": [], + "templateHtml": "", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#388e3c\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"gaugeType\":\"donut\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Simple neon gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/slide_toggle_control.json b/application/src/main/data/json/system/widget_types/slide_toggle_control.json new file mode 100644 index 0000000000..3f4486c431 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/slide_toggle_control.json @@ -0,0 +1,21 @@ +{ + "fqn": "control_widgets.slide_toggle_control", + "name": "Slide Toggle Control", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to send the RPC call to device when user toggle the slider. Advanced widget settings allow you to configure how to fetch the initial value of the slider.", + "descriptor": { + "type": "rpc", + "sizeX": 3, + "sizeY": 1, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-slide-toggle-widget-settings", + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":false,\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\",\"title\":\"Slide toggle control\",\"retrieveValueMethod\":\"rpc\",\"valueKey\":\"value\",\"parseValueFunction\":\"return data ? true : false;\",\"convertValueFunction\":\"return value;\",\"requestPersistent\":false,\"labelPosition\":\"after\",\"sliderColor\":\"accent\"},\"title\":\"Slide Toggle Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2,\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/speed_gauge.json b/application/src/main/data/json/system/widget_types/speed_gauge.json new file mode 100644 index 0000000000..3e534c217a --- /dev/null +++ b/application/src/main/data/json/system/widget_types/speed_gauge.json @@ -0,0 +1,21 @@ +{ + "fqn": "analogue_gauges.speed_gauge_canvas_gauges", + "name": "Speed gauge", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display speed. Allows to configure speed range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 7, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-analogue-radial-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/state_chart.json b/application/src/main/data/json/system/widget_types/state_chart.json new file mode 100644 index 0000000000..2b04282d17 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/state_chart.json @@ -0,0 +1,25 @@ +{ + "fqn": "charts.state_chart", + "name": "State Chart", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAB9VBMVEUAAAAhlvMilvMymeE+nNVDoetInslNq/VZqOdpuPd3d3d5p5Z5suB6enp8fHyBgYGDg4ODqoqEhISIiIiKioqKuN2MjIyNjY2Ojo6QkJCRkZGSkpKUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKCgr3KhoaGioqKisXSjo6OjvsmkpKSlpaWlwdempqanp6eoqKiosGOo1vqpqampsGOqqqqqsWOrq6usrKysw9Wurq6wsLCysrK0tLS1tbW1wbK2tra3t7e4wau5ubm6urq7u7u8vLy9vb2+vr6/xbTAwMDAxbjCwsLC3ejDw8PExMTFxcXGxsbHx8fIv3jIyMjJycnKysrK5vzMzc7Nzc3Ozs7Pz8/QuDnRzcHS0tLT09PT39HU1NTU6/3VzLLV1dXV3sjW1tbX19fYuTHY2NjZ2dna0Ira2trb29vc3Nzex4De3t7f39/guyvhvCfhvCvh4eHh6Nbi4uLjvCTj4+PkyXbk5OTlvCTm5ubm693o6Ojpxl7q6urr6+vswTTs7Ozt7e3uvhju7u7vvhjv7+/wvxjw8PDx8fHyvxX0yTv09PT19fX29vb39/f4wyL4+Pj5+fn6+vr75J37+/v8whP8/Pz9/f39/v/+/v7/wQf/xRb/3HT/5JH/9tz/++/////APs7XAAAAAWJLR0Smt7AblQAAA1RJREFUeNrt3dlT01AUBvAEd8UNl2oLrdrFolZArUul1hWlVhQXFAUF1xaxIqi4FUQUrDsUClZi4nb+Th96S9NQkjDOOKZ+3wuZw8md++M25OXMlKOccJQnTUYocdRqdw8UAqTfOjG4lvr7uowOqbtAtG6o5OiGFoNDapuJHO8tFK4xOKS9msTVgoUiaQjPclnSl01snZ/y4ly2yBNZbRarbc+3yvdpt/hLawOPJp9ur7O0mRiEmzFkY1M6N/4I8qJpulzR2sBx1sgRjQuyA2pk+STdylw2VqR/FPFfWdcCvjhduiM7kVF2db1Nuspu7JGes+I76SRba7f0Wfnn/6F6It+mfo7m8TvYvh7KTiT3PQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggPwXkFa7e7AQIP3WiUFzdl7LuBDFvJZxIYp5LeNCFPNaBn7Yc+e1KlheSYcrFCniL7LZqPl8cbp0TjavNcqu9rZJB9gdPdIJVnwjbWG1ndI95UzWM9V5rS9Ti3P4VWy1+5jXAgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAMK+FeS3Ma2FeC/NamNfCCxEQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBA/hZkTKSkwSGn7VUJOmOLV241NiRSRZ2uYcvt+Io+Y0N83USm+MqGWEnU2JCqXqI1ZE+QhYiIy36tXR5INpOQbB5kftcmK85mtdey2iJekeWqX6/3kc+TLCQTrq6eEuZCgKTsbnNXBsIZOERJMedNQnry73XpW8UAKVhIyBPScVfQE9RqEaP7iMT6g+pdw7vKbxKl3IJqV8K7OUqXPHvG9UMGykk2lz1d4g5yvNXo6QqZiA41aHT5OwQTUWBJSrWrvlMwj1jFU2f1Q8K1dCysCUmVhdenNLvMRKZt3hGNriEnxfxlGqt9aPYRkb9bP6Q1SMFrmltMukKuMT2QpckWjc/WhP2lYB/TgjzdHyCKVM/gGXnsp+qY5hbba+hIVA/ETL0+9SepsoNiTs/igGpXZIhKqVv9QVJARIfXIWqfiM1nS+qBnHdae1V7Qss8ngEijRO5a6sMCAtdqv+HfgPwpNPbU6ipOwAAAABJRU5ErkJggg==", + "description": "Displays changes to the state of the entity over time. For example, online and offline.", + "descriptor": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}", + "settingsDirective": "tb-flot-line-widget-settings", + "dataKeySettingsDirective": "tb-flot-line-key-settings", + "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-flot-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/switch_control.json b/application/src/main/data/json/system/widget_types/switch_control.json new file mode 100644 index 0000000000..95b8cc3016 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/switch_control.json @@ -0,0 +1,21 @@ +{ + "fqn": "control_widgets.switch_control", + "name": "Switch Control", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to send the RPC call to device when user toggle the switch. Advanced widget settings allow you to configure how to fetch the initial value of the switch.", + "descriptor": { + "type": "rpc", + "sizeX": 4, + "sizeY": 2.5, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-switch-control-widget-settings", + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":false,\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\",\"showOnOffLabels\":true,\"title\":\"Switch control\"},\"title\":\"Switch Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/temperature_radial_gauge.json b/application/src/main/data/json/system/widget_types/temperature_radial_gauge.json new file mode 100644 index 0000000000..5c3227e110 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/temperature_radial_gauge.json @@ -0,0 +1,21 @@ +{ + "fqn": "analogue_gauges.temperature_radial_gauge_canvas_gauges", + "name": "Temperature radial gauge", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured gauge to display temperature. Allows to configure temperature range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 6, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-analogue-radial-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":60,\"startAngle\":67.5,\"ticksAngle\":225,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":-60,\"to\":-50,\"color\":\"#42a5f5\"},{\"from\":-50,\"to\":-40,\"color\":\"rgba(66, 165, 245, 0.83)\"},{\"from\":-40,\"to\":-30,\"color\":\"rgba(66, 165, 245, 0.66)\"},{\"from\":-30,\"to\":-20,\"color\":\"rgba(66, 165, 245, 0.5)\"},{\"from\":-20,\"to\":-10,\"color\":\"rgba(66, 165, 245, 0.33)\"},{\"from\":-10,\"to\":0,\"color\":\"rgba(66, 165, 245, 0.16)\"},{\"from\":0,\"to\":10,\"color\":\"rgba(229, 115, 115, 0.16)\"},{\"from\":10,\"to\":20,\"color\":\"rgba(229, 115, 115, 0.33)\"},{\"from\":20,\"to\":30,\"color\":\"rgba(229, 115, 115, 0.5)\"},{\"from\":30,\"to\":40,\"color\":\"rgba(229, 115, 115, 0.66)\"},{\"from\":40,\"to\":50,\"color\":\"rgba(229, 115, 115, 0.83)\"},{\"from\":50,\"to\":60,\"color\":\"#e57373\"}],\"showUnitTitle\":true,\"colorPlate\":\"#cfd8dc\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"valueDec\":1,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"°C\",\"majorTicksCount\":12,\"numbersFont\":{\"family\":\"Roboto\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"unitTitle\":\"Temperature\",\"minValue\":-60},\"title\":\"Temperature radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/tencent_map.json b/application/src/main/data/json/system/widget_types/tencent_map.json new file mode 100644 index 0000000000..25ebe39e8c --- /dev/null +++ b/application/src/main/data/json/system/widget_types/tencent_map.json @@ -0,0 +1,21 @@ +{ + "fqn": "maps_v2.tencent_maps", + "name": "Tencent Map", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Show latest values and location of the entities on Tencent maps.", + "descriptor": { + "type": "latest", + "sizeX": 9, + "sizeY": 6, + "resources": [], + "templateHtml": "", + "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.24727730589425012,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.8437014651129422,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7558240907832925,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second Point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.19266205227372524,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7995830793603149,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.04902495467943502,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.44120841439482095,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"tencent-map\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details
\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.5,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"Tencent Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/thermometer_scale.json b/application/src/main/data/json/system/widget_types/thermometer_scale.json new file mode 100644 index 0000000000..f2e5c3fedf --- /dev/null +++ b/application/src/main/data/json/system/widget_types/thermometer_scale.json @@ -0,0 +1,21 @@ +{ + "fqn": "analogue_gauges.temperature_gauge_canvas_gauges", + "name": "Thermometer scale", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Preconfigured widget to display temperature. Allows to configure temperature range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 7, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueLinearGauge(self.ctx, 'linearGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-analogue-linear-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 30 - 15;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"defaultColor\":\"#e64a19\",\"barStrokeWidth\":2.5,\"colorBar\":\"rgba(255, 255, 255, 0.4)\",\"colorBarEnd\":\"rgba(221, 221, 221, 0.38)\",\"showUnitTitle\":true,\"minorTicks\":2,\"valueBox\":true,\"valueInt\":3,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"colorNeedleShadowUp\":\"rgba(2,255,255,0.2)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"highlightsWidth\":10,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"showBorder\":false,\"majorTicksCount\":8,\"numbersFont\":{\"family\":\"Arial\",\"size\":18,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#78909c\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":26,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#37474f\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":40,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#444\",\"shadowColor\":\"rgba(0,0,0,0.3)\"},\"minValue\":-60,\"highlights\":[{\"from\":-60,\"to\":-40,\"color\":\"#90caf9\"},{\"from\":-40,\"to\":-20,\"color\":\"rgba(144, 202, 249, 0.66)\"},{\"from\":-20,\"to\":0,\"color\":\"rgba(144, 202, 249, 0.33)\"},{\"from\":0,\"to\":20,\"color\":\"rgba(244, 67, 54, 0.2)\"},{\"from\":20,\"to\":40,\"color\":\"rgba(244, 67, 54, 0.4)\"},{\"from\":40,\"to\":60,\"color\":\"rgba(244, 67, 54, 0.6)\"},{\"from\":60,\"to\":80,\"color\":\"rgba(244, 67, 54, 0.8)\"},{\"from\":80,\"to\":100,\"color\":\"#f44336\"}],\"unitTitle\":\"Temperature\",\"units\":\"°C\",\"colorBarProgress\":\"#90caf9\",\"colorBarProgressEnd\":\"#f44336\",\"colorBarStroke\":\"#b0bec5\",\"valueDec\":1},\"title\":\"Thermometer scale\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/timeseries_bar_chart.json b/application/src/main/data/json/system/widget_types/timeseries_bar_chart.json new file mode 100644 index 0000000000..2ae875c925 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/timeseries_bar_chart.json @@ -0,0 +1,25 @@ +{ + "fqn": "charts.timeseries_bars_flot", + "name": "Timeseries Bar Chart", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABOFBMVEUAAAA3oPR3d3d6enp8fHyBgYGDg4OGhoaNjY2RkZGSkpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKChoaGioqKjo6OkpKSlpaWmpqanp6eoqKipqamqqqqrq6usrKytra2urq6wsLCxsbGysrK0tLS1tbW2tra3t7e4uLi5ubm6urq8vLy9vb2+vr6/v7/AwMDBwcHDw8PExMTHx8fIyMjJycnLy8vNzc3Ozs7Pz8/S0tLT09PU1NTV1dXW1tbX19fZ2dna2trb29vc3Nzd3d3e3t7f39/h4eHi4uLj4+Pk5OTl5eXm5ubn5+fo6Ojq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fH09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7/xx////8KXFhiAAAAAWJLR0RnW9PpswAAAvtJREFUeNrt3GtXElEUBuDpZlAqylVHCyMwyi4SpkaWAl4qk8wwUhJlmOn9//+gLzbKbc6AAm5991p82WvPOedZZw6zYM3aGupCQ4tYkZDSsKDrocNVvz8jHQKYY1ZiT/6OAJllRPSpsnzIqIG9SjYmHpKfAzI4CIuHjJeBtyHfZwCatiI1/m+BYV2Dw35NniOEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQUp/6Wx+DgbRbxOAhEb/fXJ98akiH1AIAHtSWlqRDSvdDGcOHnYR0SPV7zVsaQyHeO0jTJapEV5C9bUz9fIjsvHRIefhRxHqpjxzA3ftajXOudHGJOtHV+1rV04/wHenDc2QQkFbLJISQwUDaP90IIYQQQgghpAvIxRO9gFxG4lZ9XBFIx6sihJD+QVRjEEIIIYQQcoMgahkhhBDSe0iLX3ydQdSLIKR9ghCJkIYEIYQQIhPS8d+AhPQboppFw9HMxBrs/lqCIbObpgezP67YjnQ8iwagHDzrryUZUgsUz/prCYZY05vn+msJhiwM6XrR7q/V9OJU4xQuEpcxRsezNPbXEn3Yzz9Hulg3IYT0C3LxxI2CNCQIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQlpA7P5a0iF2fy3hkLP+WsIhVbu/lvRby+6vJR1i99eSG6ffu1XHxlRNcfWq3I0iIK4tJK2nXVyV0lOqEmvjGWAtvnCu+jMzkQWMiOlYdRSd3MAH/UnVPWR/ApFfSkcpiOBvRc3XtAeYW1ZUJbZMD5C8azhWLX4xvZVR692Se0huHm9ySogxkhs3lFVewPM4WlFUlUMoJEYUox1+jAFIbLuHrKaQWlMu8TicDp+4gdw7/qS4t2qBohk4UUF2nieBfLyDM/ItgXhBucT113i14QbixW7M+SRNb6EQ0u8kHavyZQxj2/kgNUCsYDRoqXfEF/Mdu4G8D43uOtakh3R9H1DsyKZvOmneDjt+D/0DTzolrPMHmggAAAAASUVORK5CYII=", + "description": "Displays changes to timeseries data over time. For example, daily water consumption for last month.", + "descriptor": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}", + "settingsDirective": "tb-flot-bar-widget-settings", + "dataKeySettingsDirective": "tb-flot-bar-key-settings", + "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-flot-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":true,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"defaultBarWidth\":600,\"barAlignment\":\"left\",\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/timeseries_line_chart.json b/application/src/main/data/json/system/widget_types/timeseries_line_chart.json new file mode 100644 index 0000000000..2dd4b07d6c --- /dev/null +++ b/application/src/main/data/json/system/widget_types/timeseries_line_chart.json @@ -0,0 +1,26 @@ +{ + "fqn": "charts.basic_timeseries", + "name": "Timeseries Line Chart", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAXk0lEQVR42u2deXwb1bXH+Y+lfY/utK98WkogZSk7LcujBF4pAT6lLaWvlBZeCrTw4AGFvrbQkPYRwlIWfyCr7WA7XrN4wzZx7DjxIu+2vEteZHmTJdmOd8faR9K838y1FUWWZVmaGQM585mPPmNZR/fOvV+de+455849iw/7OOuss/gojpaWFhI8cwQFVlQqVbp4JCUlzc3NaTSaveKhVqsJLBKMHCx2eDyeuLg4r9ebl5c3NjZGGosEpQEL+qm+vh4XKSkp0F4ZGRkWi4XAIsFowdqzZw/HcbiAurLb7Z2dndnZ2QQWCUYFVnd3d1FRkU914XV2djY5OZnAIsGowIqPj/cNfOXl5bDiY2Nj+/v7/anC0UIHHeEdwZUQxkRY8WS8k6AENhb5sUiQwCJBAosancAisEiQwCJBAosancAisEiQwCJBAosancAisEiQwCJBAosancAisEiQwCJBAosECSwCiwQJLBIksEiQwCKwSJDAIkECiwQJLAKLBAksEiSwSJDAIrBIkMAiQQKLBAksAosECSwSJLBIkMAisEiQwCJBAuuzK5jV7SKwCCyJBd+pd35nz7x6xE1gEViSCb7XIFCF84lCO4FFYEkj+IFaoOqSuPn1cfMX75nvnvQQWARWtIKvFg6CqnWx8+83Op8ptuP6hWN2AovAikowsd3FqIppcDaOuIv7uUtj5y+JnR+a9RBYBFaEgvs6BKow9r1V6wBV7PzdYRve3KJyEFgEViSC6VrXxSJVL+YN+6jC+ZHOBQV2Wfz8uNW75lXVTni2q50/zbLWGN0E1qdAEP4q0AOqXq92ZKi6/cHC+auPBKX1dp1zTarq9vBweWytctyaamETVZy3pFim7V6FwMKOX2yPQmwup9Pp0tLScnJy2J5NBFaII7uHA1Xora3Vwgi4FKz9ncIQeXWC5aTTq1hVwU1OD/fMUfv3PjzF0w1JlieL7HcftOIaF0qA5Xa7ExISfG/t2LED79SJB4EV4ijs4+BWQD+9orIzjJaChfMnWUJfxrY45a6qYc6T1O56pMB2ady8j6cfpllfOOZI1bgaxMpgSnGVSNtbRf2ygzU9Pf3mm29iYybs+wUthe2Z8KbRaMSOmATWcgd6iFH1t3K7j6GgYCWIs8Ub91nsnPRVbWpu0Yx7Pmh03i/iy851cfP3HLS+WuU40s8trc/OJsHTtj52ric8H1vkYLlcLrPZDC21fft2QIZ9VvGmyWTKzc0lsIIeRwc4phX+Umb377OgYOH80X5BSaRpXBJW1eXhXy53XL131scTBtxNH9t2qJ2Vw1zQavjOxw8LPrb7DlkdbjnBws6XBoMBF9hVdXx8fOfOnbjGRpilpaUE1tKjwuD+brzQkc+V2AM6bDmwdoju+NvTrW6PZFVl4UicN6dY4YyFF63O5A7Nk+8Eed9PnIHstmqHjGBhp8Jdu3YdOHAAJjx2kysuLsbWvbt378ZemP5U0X6FOJLKdevjTqJLHs0cAUZhnukV3awjPyjRS1KNA6qudXtOrgMZhYPhV8P/fLe4/5LYk5jMJpTp5N2vEANi0GvSWL4DHqDL9wpKAhoiqCZYTmPhfLvOAcG7D1g93miranV578wQLKo/lTpClBj6hOAWlTAg3rDPsqKbjfxYMgo2jbqvFOdTyFloWL63lutIjFPf3yeIlw5xUVZ1c4XA6J0ZllqzOxqw6s3un4omPywzL4G1JoLNY27mE3rssB390bh6sHD+X6UAxC9ybNFUtXLYjfELqRPZ3dyKJYYGC6/HBrlrE4X7golGYCkniN9xy5h7W42DUYXYXwiqVuxmmMyYuOF7GszuyKoKz+dNycI3/KPSEU6J4VR1T4sTpGI60jnhIbBkF+ydEpxDd2Sccg49nG+rN4XbW8udfy1ziGrPFllVnz0qWEX3HrL6+I4eLJxPiRk+Pz5gtXEEljyCjKd/3zd9KhKyzwJT3ee5jhKsMgN3+V5B5cCrudqq5uo4CF7xoQUe//BLDKeqNSb3hnSLtIkYBJZwwAeNJKr/2G/1j6yBpzRtWDytqpuZ1nm+xL6qqo7Me68RjaF36hyrLTGcqsJiWy+6fIsHuGhatc7sZhH3Mxos/bQHmcQYAnw8wZLFuPDGkYHQtlQ0YBUtJgAitBdmVWHq/bZAyJJArkQEJYZZ1deqhWH6+iTLqCWSePngrOepIjtrRrTemQWW082PznvbTgjxsnsOWf2DIU8ese9rdzGeJOytEBGVVyocYd5jkhhtBPTHBzn5wIJu/kWugO9v8m2nnG1el3CGbNVZh/f1GgdTeBjoETxFVFResMb82JcbLLTFpM0LIwnauEDPJXe4/ppvQOfhZ/TLXBuGuasSTuWQsBNx/scL7QltrgCrXG6wPupdSAA8seiZDHGPUKvMK4vpW8QlhikIcK9LspyWi9H7DF/1Rb7jXn74XX6+GaD5V5Xz8Cka1/WiCO4IjYkoaqO46E1esKC64VeUFawdTc6NB63wPbK8qNAnxiAYT3ekWx4rtMe3LhtckxssnL/OE3TDPxcTAJe7R/QcS1tAnylT1Q/bhGxYhNih13nj+7zq7NPO2gtnau/jRxJ4x3DZEHfXolWK1KDM7lPaVF6wgD8zC+QDyzzvZfFgdkIbIyKLjJEHc23wJm/KHEHqyHv1Tnj/cns4mKVhWk4KgHWgU2gcuMfmHN4Q9xgjLlG8KcW6XMKCHFV9tkQYqV/K+YivPI9XncOP7uPHMviex/m6i/wh6z16TVLu83/Oyo1tnAr4BhnBwsDkM2KqZUuy/psY3IBNUAJoTGvAx3KC7QbdaMfLjSOh0ldYOGVXk3O5e2wdcyPlC5oYzg4lfwPwPjyRqZ4v+4oAkOGN0zy009pDha+XHb7fWv4lH2Heys+fbLzTpHmta6BGbXbICxZSY9m46wtiSA4WZlXQ2Ehqy+91rZXiWU5wuvnXaPHxtqcbR5ZlImkxARBuyaX3iDeZ++OPxxwKK9cW45i1ej3qX1DwcIF+wXJH5hYML2anAvfnik826yrN2q3zDbd7Kz/ng8xd9dWZpgd554gsYCEHbUO60Chv1DpYKAqRV8nB+suCF9u+hiNaUMHOgXphBBEberLlUfYjDnqyDHQopKX3+HcxsIjE4hqToqO22mw/qb4LNR+tvPmKuBOYLxtPemDVsA7FeVfKBOwKf5Em01xP37GRjs2W+ptx457K83mPTRawEtuE3yKcubBpYOWwZEUk0UoIFrwm+N1cFj+tbXt/qvm3zaaZTwxYHH7E6Jjp5l+5q74iXjykHrEF/fwuMUv4tjSLurk1aKQ5q4dT2BycaP096uysubh12PSQOMO4cnFFBiY90LKhS2wbNuj6imUZCi0u741ifgimXWzAZn/uOq6XECwsXX8k5bC5/FqmGGz117cZBj4JYOl7s1AfV/WFYL1zoJGr/gb+nG26v8k8v/TzcB1BJ6Fx3i/p890azHm2WmuLyq7wPGO4813U1lP5hc6BBvxZbuBYqg/0FhRE+E4+WcBiE5n7Mq2+Yt6sFZTWbUlTbq80YA2NaisL72VI2WuvstddLfRlzbfRkWsL1gGVxl57JSoz2B3P3tEONnPV38Q7c+q7m0yzS0XeqRca5/bkKV/bPF8SGGlWBqxefQGvOhdjmb43x/cmVnj/6bjDf066NmDBRckyTDK0tq6BamPnWzp9Yb3RCS8A3sQqvGjB4qZ4/YselWAtWisuMHR9AJug2TQ517hR/Kl90b9RlAerSfV3QX3WXeNvV2mGNK5qYaKOIbLZOLVcAiBSo3B/Bb0c81/DalbSM6IZbGcDt0n7RvQlSg9WjKpn84Hddccecldd4JspQJc01Lx8R0IH1L7LEylYXk7wy9UKv35Pxbl5BY+W6Uf8TE7HifbnxOLOMWtfWxOwwLdDJdx1r/7jgH+1G/odtZcJP4b6H2DOFZgAKJqhD+TYEKi4TpzrvF3nUGwCK04Dxx11l6N6Uy0Ph3aRKAuWtZs3x/KdD3lqvuHvQLPVXTve+hSGqgVvh+qc+iN31jan8B7rqsGaOsqrr2Hf01V+z8bExv85GsT+MHa+Iyrzs1EuNJnCYGFahKIxpVrGqh1ko6St/rrWYXNAAuCV8XPChEv0LyAAFV6JLoyz0d+j2mydb9ggKNTG25rMFkkaJwqwnKP8eLYQSKq/xB+mE6XfUpc+NNi9t214yFdM56AaHh1nxaJLrfqrfM9j/HSpGLZf6bD1CjEEJth45XBfFkt3LB4I/sPS9+ZiQBRsmsaNWapGxcDCbEgs95wAO8//bB0esdbdgLo5ar/bbujz/9cT2WZfOLxkgFsRqT5dJgZcfNWM6qqBnlS12RnxPeJHKM42LmobHpaqcVYP1vghvvdpvvHy0+JHdd/muzeN6JM2JHRihlzUH7xdDqmaYvLjmotuOyWovlaIRrnGg5fHTfN9/8tXfl74ZM3XhE96nQiZregzxIwGg6/AlurydoNeGbAmWzYJNrvqgdAfazGesNbfJMznay9tH+o9FaEr07EEQKxADCEO7YJpAbhcdHl/zjc49OvSQzjMlrtHUcef7an6knawRcLGWT1Yi34/vvrL/hFv/PcRMWEoRJejQvEtwoTxgfQ218Brp1QdAlL4Kug/X3rGgjl1ofiBc6dqf8Y7T+DtjnHPxaJhuzR7ZIn+GGb9h9l+d79KbrDQK6gnHIN5qrKVTTHjFHN0uWq+BZPZVyIidI8V2kMitRfuJdZoMInwJzyTtap3HbVXLLxZu97QtT2c4YyVqNMf8YrRwD7dIWkbZ/Vg9T7LG2NEmE4zwrHgjoVUkXobukIbDy4GyEDP5Me89kHeFxCA5ht4hR/P4tXXLbzTvpG3dPhmhYgrC4vpjjvCM6VnzKofiVPF8/t0+2UFC64EwVXd8XKYgvA7zKl/LHL/TaYqIIiEk4pgrddsmsbkmvksBPO/7kaMg75IEQRhTQ707LPXfo99APAZunYEdZv5n4WqfE/Vl/F5fLnkjSON8Q59hRmN/0NXQlQI4Qt8Elm2M45FA8s5xpt28k03nja8Qp+NpfvPCptH3cwLHILdwEcIqbRj7S+yqSIs6xXnO5GBhTmggEjV11tM4+ELQgMJMTUhuHZB10BtUEFM1jDD5aoWpkSYUYpIcctU1QVHlBhUOZshC1lMVJcbkedV68Rp4G/CbxalwTrcx7Ek8SpjWBVij/WBHzXwi+bqeN1TfP13+OH3eE9gdiVSGCCFVS6rbQJD105R4SNstwndKS1YsJph3+DLMQatlkjEeRDtEdn6yjFVeoCZD/3HNAqbaSIYF15VuV59nqXh1gW8qr4OvEB8ANNsLJ5v+GGY4+YagIWnXLBJsv+zN0NXCKlIF4u6ZzK89bcAC4+lY0NtuYGLoAkQvXJXf401ZatxVEKwYOiIxs1lDNnVjqHgcqL1CYEA1fmoJPNKQMuyia1Y4Q09faURVLWrv2qm6efsSwAovrPVaFycBj6JNy2qi4CvTDMbCcDC4zfR5bemWle15u5BMbcaidJhgsXyLf2fRLXaJtAOtTlr1jGzt8PQJQlYMJXY9FMcoSJ2gLngixGneP8K/6RX9S9syjLd/J9w00TZzd39lbNNP1nAq/ILY20vjHS8wlCDjSWfLyZasJAzjwdaosuxNmFVFcJiI5b0jaUNK5abVNbDHDwrPvYpdBMgXG9puIUNEJGpgYATowzTgj4zJVJ3JdetenzRg3DeZMt/aQY7JOxmuNamm355akavOrdXny+r9zhasFjWx10HLBEs5nw4P9xHVW9MnQgz1L9iEzSZT0ITMA8Qpk7RtB0whftHiAQMVEsSYDFrXp1o/QNST2WawAJWIAtw4buSO94VFVhY9MOS+LBqKoIKIe0TyZ9IAfVfXrf0QMY+m0WuVl0t3wQucTgQfr4TrY8HtV7Dabvxtv9mSVefkJzVsCMEQwqUGBVYsJDE0Kk14gptEp+vj0TQEIX+PFv4DMK00jYBxgIk0bI5fASJXJohLXQecr07DD2fLrCUEYwcLCyWxWI3TO4OdkXuHCrsc7E1wVg6F7RE+AwF62rvbLVR+iaA15tlHHDV/xZgcq3YdkjcgyCmWp9tPtYArD+XCjE7hHGirBDWH7OHeQb1u94nrvN5/iOjTE0AB8+s+j5mchk73w5TsKevjDmflnouCKyowOqbEfLNcX4cXjJaiAohSQGpCtB8Sx/OVCj6XZHWnFLRI18TwJMkprucwzyoLBISUtDFvNv+IBJY0oD1e1HNPF1sl6RCbHnkH47Ygy5LRFqzAm3X13uQTfEQuoaLMoQgclRYPA4TTAJLSrCwipKlGBwblCbGhJVhLGMEz87zlZUvZuj+IDnax2yuIkNhqI2loyDKdlyVFtwjarY4a4SkDOSonAl8KArWw3mrjtmtWCEkLDCLbSFM5F0IE2GhgZJth6DvnPpeMdn1vKAjHRIBWH7BcstQCawIwTomPo4BTnCVgZOwQogAsg1eak1CnbLFVdS3pCyEiZRsu0WTS1xr2vo7/+QTZASwgONy8WACK3Kw2DSN7XQlbYVeLrezdG9ucRU126d0TdquWvU+CwMLQethE3vzRNsfhVTgpp+dOXwoB5Zo96yw6DviJwqzRyE+J9ryt6VJ/PzW1Qoi/46ZXEgGR74U0ogRHkY8JHQUj8A6DSybzZaZmZmamqrVavGnRqNhexdiM7AAsGC2v9fglKlC/xCfVsDOD/xSv9eq7bBOiz3FAHkHWGnNFv+cUXxEC1Z+fr5er8cWmDExMezP0dHRoBoL1k9kD+cMp0Jwr98gLt3ckGHxL2UN2w7LE3wmF1JNfMMigbWKoRBbNSUmJvLibqvYpAk7geGdALAiq0r4FXpdfL7qrtOTcNa87fp1GTC5TJqtZxofEoDFNlmdmJjgxV3m7HY7tpXLzs5WGCysN0dOX8Mnr+2Qcxf0yQsEViiwsJXc/v37MRoygJhphT3lkpOTFQaLsUW99RkBS6VSbdu2LV08oLTKy8uxjS828O3v7/enCkdkW+PReaadp+1XeNoiZI6DGltqvCugsUjwTPRjEVgkSGCRIIFFggQWgUWCBBYJElgkSGBR25EggUWCBBYJEljUdiRIYJEggUWCBBY1OgkSWCRIYJEggUWNToIEFgkSWCRIYFGjkyCBRYIEFgkSWNToJEhgkSCBRYIEFjU6CRJYJEhgkSCBRY1OggQWCRJYJEhgUaOTIIFFggQWCRJY1OgkSGCRIIFFggQWNToJElgkSGCRIIFFjU5gEVgkqBRYOp0uLS0tJycH+1MQWCQoGVg7duzAZmB14kFgkaA0YEFLYXsmXBiNxry8PAKLBKUBy+VyxcXF4cJkMuXm5hJYJCjZULhz5068YiPM0tJSAosEJQOruLgYGxfu3r0be2EG7FdIBx3hHkE9CxgQQzgdeGUPKvHTWKLS90PHGXIQWHR8AsCqqKiA+YVXxepXVlaGEvEqd0HYqri3t7egoMD3Z3V19ZEjR+Qr0Wq1ZmZmpqamYp7k8/WkpKR4PB6ZSrTZbIcOHYL3G3eKP5ubm9G2WVlZISwfJcCanJxEK+ACNz81NaUAVdPT04mJibjA68zMjKxlDQ4O4gcTExPD/iwqKqqvr5e1RLgJ+/r6gJGv0MOHD2/ZsiUg4CHhgZ/KwMAASty+fbvdbt+zZw9+P1VVVQGecKXB6urqYg6I48eP41oBsNDEmJyirPj4ePma2/9Ai7MLdPbBgwfx+0YHyFqixWJJSkpijkOghvuV9U7n5uZaW1uzs7P9+TYYDGsJlkajYUMSXrVarQLd7HA4kpOToUjw6nQ6lQRr69atKL2jo0PW0RChs4SEhImJCWgRXOAe5QYLAZXCwkKoRvZnd3d3gBt8DcAaHh5mQR5UBT8vBboZ4fCSkhJcHD16lJkFioHFLkZGRvx/3JJbdfv378doyNQV9BYsns2bN/t6XfIDGEFB4mLXrl14haKSz6Q7a1UNAVsHowNeca2MxkLgEl2LV1wrCVZDQwN6GlYI2JKpLGjibdu2pYsH7Ff2pqwaS6/XoyVBM9gFTy+99BKGAplmY/8Pl7O7ukBGoYYAAAAASUVORK5CYII=", + "description": "Displays changes to timeseries data over time. For example, temperature or humidity readings.", + "descriptor": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}", + "latestDataKeySettingsSchema": "{}", + "settingsDirective": "tb-flot-line-widget-settings", + "dataKeySettingsDirective": "tb-flot-line-key-settings", + "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-flot-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/timeseries_table.json b/application/src/main/data/json/system/widget_types/timeseries_table.json new file mode 100644 index 0000000000..0432c36731 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/timeseries_table.json @@ -0,0 +1,26 @@ +{ + "fqn": "cards.timeseries_table", + "name": "Timeseries table", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays time series data for one or more entities. Data for each entity is displayed in a separate tab.", + "descriptor": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "latestDataKeySettingsSchema": "", + "settingsDirective": "tb-timeseries-table-widget-settings", + "dataKeySettingsDirective": "tb-timeseries-table-key-settings", + "latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-timeseries-table-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/trip_animation.json b/application/src/main/data/json/system/widget_types/trip_animation.json new file mode 100644 index 0000000000..5a5b57e92f --- /dev/null +++ b/application/src/main/data/json/system/widget_types/trip_animation.json @@ -0,0 +1,21 @@ +{ + "fqn": "maps_v2.test", + "name": "Trip Animation", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Trip animation on the OpenStreetMap or other map providers. Allows to visualize location and other timeseries data for each point in time.", + "descriptor": { + "type": "timeseries", + "sizeX": 10, + "sizeY": 6.5, + "resources": [], + "templateHtml": "", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n var $scope = self.ctx.$scope;\n $scope.self = self;\n}\n\nself.actionSources = function() {\n return {\n 'tooltipAction': {\n name: 'widget-action.tooltip-tag-action',\n multiple: false\n }\n }\n};\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-trip-animation-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 0 : (value + 2) % gpsData.length)];\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 1 : (value + 2) % gpsData.length)];\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"mapProvider\":\"OpenStreetMap.Mapnik\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"showTooltip\":true,\"tooltipColor\":\"#fff\",\"tooltipFontColor\":\"#000\",\"tooltipOpacity\":1,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"strokeWeight\":2,\"strokeOpacity\":1,\"pointSize\":10,\"markerImageSize\":34,\"rotationAngle\":180,\"provider\":\"openstreet-map\",\"normalizationStep\":1000,\"decoratorSymbol\":\"arrowHead\",\"decoratorSymbolSize\":10,\"decoratorCustomColor\":\"#000\",\"decoratorOffset\":\"20px\",\"endDecoratorOffset\":\"20px\",\"decoratorRepeat\":\"20px\",\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"pointTooltipOnRightPanel\":true,\"autocloseTooltip\":true,\"useCustomProvider\":false,\"useLabelFunction\":false,\"useTooltipFunction\":false,\"useMarkerImageFunction\":false,\"useColorFunction\":false,\"usePolylineDecorator\":false,\"useDecoratorCustomColor\":false,\"showPoints\":false,\"showPolygon\":false},\"title\":\"Trip Animation\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":false,\"showLegend\":false,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false},\"displayTimewindow\":true}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_boolean_timeseries.json b/application/src/main/data/json/system/widget_types/update_boolean_timeseries.json new file mode 100644 index 0000000000..7054be76be --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_boolean_timeseries.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_boolean_timeseries", + "name": "Update boolean timeseries", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAIQSURBVHja7d3PSxRhHMfx+VcWgmg3ltKD7BJshyBIQusQ3vIYCILCRMyhH6xShlGWMGq1p1gIibxFULRR1F4ixcNqdNhmUXFipWnTYvfZ/TrjJqFQ13ge3p/L8MztBQ/P8/0eZr6WNHyvrHk8vy5WoxIo0TwqqDQsPxADEviWp0yAKM8qixEpAwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC5H9DSiP2noyUNIXsc9j2qKYQ2/732lzI7LyukN4jUV7uLrtndIWcuP4lzKYBELf9rD66/bzZhqw9vFcM37ReTzz5pR3kc8fAWPriDmT58NDl5F0R51j2zOm6PpB+13VfyNNbIm8O/Iwg430iz87Lu/iaNDKz+kB6Hcd5HH60PDedjfkR5FXi2oeWyI3j+Xy+56puW2vh6KXcnTZEFq90pQripAbC5HSDDDoin9qQjxVRkwk1dUrLU2v4bLAxGFuNIEPnanK/s7WSeNDcct7rBvFOxg7djBUjSLX/YDJdEHmbiccv1PQrUb7++T/B9/Xfd8sPikbK+L81VqP7HEu0ukCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAADEUYsyAYDNGNn/zrboZQ7SVZcZYcyXbTFGWg8aBYQQAAAAASUVORK5CYII=", + "description": "Simple form to input new boolean value for pre-defined timeseries key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Timeseries key type and boolean value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{currentValue}}\n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\r\n overflow: hidden;\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n display: flex;\r\n}\r\n.grid__element:first-child {\r\n flex: 1;\r\n}\r\n\r\n.grid__element {\r\n display: flex;\r\n}\r\n\r\n.attribute-update-form .mat-button.mat-icon-button {\r\n width: 32px;\r\n min-width: 32px;\r\n height: 32px;\r\n min-height: 32px;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .mat-icon-button mat-icon {\r\n width: 20px;\r\n min-width: 20px;\r\n height: 20px;\r\n min-height: 20px;\r\n font-size: 20px;\r\n}\r\n\r\n.tb-toast {\r\n font-size: 14px!important;\r\n}", + "controllerScript": "let settings;\nlet utils;\nlet translate;\nlet http;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [{\n key: $scope.currentKey,\n value: $scope.checkboxValue\n }]\n );\n\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-boolean-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update boolean timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_device_attribute.json b/application/src/main/data/json/system/widget_types/update_device_attribute.json new file mode 100644 index 0000000000..0a4608ef54 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_device_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "control_widgets.update_attributes", + "name": "Update device attribute", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to send shared attribute update when user press the button.", + "descriptor": { + "type": "rpc", + "sizeX": 4, + "sizeY": 2, + "resources": [], + "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n
\n
\n
\n {{ error }}\n
\n
", + "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n let attributeService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n let entityId = {\n entityType: \"DEVICE\",\n id: self.ctx.defaultSubscription.targetDeviceId\n };\n attributeService.saveEntityAttributes(entityId,\n entityAttributeType, attributes).subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n\n );\n };\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-device-attribute-widget-settings", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"entityParameters\":\"{}\",\"entityAttributeType\":\"SERVER_SCOPE\",\"buttonText\":\"Update device attribute\"},\"title\":\"Update device attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"targetDeviceAliases\":[]}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_double_timeseries.json b/application/src/main/data/json/system/widget_types/update_double_timeseries.json new file mode 100644 index 0000000000..38b5a8e501 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_double_timeseries.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_double_timeseries", + "name": "Update double timeseries", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAANMSURBVHja7d3fS5NRGAfw/SsrhSCKKIugburCi6KCLrropi4izME0c1kLUxquHybmKNPAi4qMJkm9hOkswhVkpaLMfhmlbjHYu+3Vaf7a3r3fLjad5nZh6NiZ3+diO+fAe/HhOc85Z+/Fjg4R2T0ieLjlMHQRT0iF4KGGPBGdHEIWREjWudVsgKhu3QiyIkYIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQghZS8jbbwDQOT9slJc/5HGJAOmoAaAUjKWERCO9z7RI5kPGC8cBxy3A5/wUBoyy1qNirg+I9jtlAPBcvGkt7xWgRmyvgKpu9JR3NF4HjHLk9DSCBmi2eqnsFwCEK8vGRSj2j9fgM85h0Ito4VgC0m/R4GwEgHZ72xMRIOHiwIv7wJ/HN6oKfAmIdL62tsoCAMps1CvE8vuwvWIIaHgehTEGmULQgPYGv9+vCLWP/Cg1a4ClG18LvDDKKPmJPgNGzwXgdQkF0S5JAPrPXqgzDcAo402R2WYAukorzUMi7uzR6YWqmY0BJzQeUQghhBBC1h9kZllDDIj2cumBaKZaijWk6hmRIFqLybl0RDJJi75EgWitJse/Y20mKf4hDkRrWe4AJJOUPB8ZC0mWj1hOUjgyDjJ9zw1Ae5rcsUKIbK9rmVzovatv+hJvfh5ec4hirfidYl6teGp1bd1/Km9XfDhasOnksZxGAIGmA/rWtU/J2NXL7tT5WEmxz+0sUqHsNsR69lwXcDc3AC13b0U6IFCsZanzsZLld1D/AYApP/5wDYBBfTfwPupPCwSKNbnj/zbE44WLOvYcLwCkCYLpVTyiODb2zDe/P7qypQnphKzioXF0myUxMw9t3+MUFBLcd2LxP1qo1bnDQkKmjhydWjqw4YGIkMiJ/MT5ufggAL9eEhCilWzudLlcLgWvbYBDfyfoPpOnCAiZ1MeiGeYdKtCcp9cfHhB11YoXeewFo2+Cv9kJIYQQQghZf5DbaQ1mhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCEkMyFZc0FwdlzZPC7rwtlxibaqy45rzVX8BUWnY6Q1/jMIAAAAAElFTkSuQmCC", + "description": "Simple form to input new double value for pre-defined timeseries key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Timeseries key type and double value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n dataKeyOptional: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-double-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update double timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_integer_timeseries.json b/application/src/main/data/json/system/widget_types/update_integer_timeseries.json new file mode 100644 index 0000000000..52039b96e9 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_integer_timeseries.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_integer_timeseries", + "name": "Update integer timeseries", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAANBSURBVHja7d3dS1NhHAfw/Sum1IUQBZYJIdSF4VVQIOSNXZiJg5nmWi18CXG9WJgSqd1IBUWu10PY1EBcIFYqyoTCLOZWA8/0uGmbbjs73y7mS+Z2YdjYs76/i3Oe88C5+PB73jhwnkeHiOxyCh4uOQxdxO1XIXiofndEJ/uRBuGXdS41HSCqS+dEWoSTEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYSQfwl59xkA+teqDfLWl9wOESC9twAo5QsJIdHI6EstkvoQX4UP6LsNzNo/hgGDrI2oCI0B0XG7DADuSzcttaMC9JG2t0DTMEZqezuvAwY5UhbEvB5a213J9A0Awg0mnwid/cM1zBpCmPQgWrGwARlv1GDvBABbd88TESDhc3Ov7wM/H99oKp/dgEgXWlqaGgFAWYl6hBh+H9rqp4COV1EYYpAA5vWwdXi9XkWoeeRLjVkDGofxqdwDg4zqrxjTY+b8HDwOoSDaZQnAeNXFVuMEDDIGKs1temCwpsE8JeLMHg2u95qVGHBR4xKFEEIIIeT/gyxvKYgB0d5sXhAtN0uxgtS8LBJEsxrtm2sko/TbTRSI9tzY92ddj1FavYgD0axbHYBklOLnI2Uh8fIRy0kCR8pBgvdcALRn8R3bhMjdrdYlAFh62mpd/Uk81J8ciGKp/56gXW27aQ3uPVqac9AJTOUePpOfMw1gom5/VpJSsnC1zpU4H9vp7KEDlSqUQ3qg6GQI4cKzwIvMU6eTBYFiMSXOx3aG38mM9wCMBUDDAIAr+YDzB9qTBoFiie/4uwmxuGJ1+CgqAYBkQhDcwSVKX+YIANi6SvKmkw7ZwUXjzL5GAEDlsT2l8wJD5o+UrO1o4S0sFhcSOH4isP7wYFdAVEikpEABgGBWF4D27KigEK06u9/hcDgUlOUNLQ7lmkRtWksZsXiEhaqsjN3mgMij1lqEPcJsY8OPD4QQQgghhOws5E5SgxkhhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCElNSNocEJweRzb7ZF04PQ7RVnXpcay5il+KVmW7YpZ2rQAAAABJRU5ErkJggg==", + "description": "Simple form to input new integer value for pre-defined timeseries key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Timeseries key type and integer value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n\n
\n \n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}\n", + "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n dataKeyOptional: true\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-integer-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update integer timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_json_attribute.json b/application/src/main/data/json/system/widget_types/update_json_attribute.json new file mode 100644 index 0000000000..3ca5f234ee --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_json_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_json_attribute", + "name": "Update JSON attribute", + "deprecated": true, + "image": "data:image/png;base64,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", + "description": "Simple form to input new JSON value for pre-defined attribute/timeseries key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. JSON value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.jsonInputWidget.onDataUpdated();\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}", + "settingsDirective": "tb-update-json-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetMode\":\"ATTRIBUTE\",\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_location_timeseries.json b/application/src/main/data/json/system/widget_types/update_location_timeseries.json new file mode 100644 index 0000000000..f8902c3f55 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_location_timeseries.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_location_timeseries", + "name": "Update location timeseries", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Simple form to input new location for pre-defined timeseries keys.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? latLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n \n \n {{ settings.showLabel ? lngLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentLat: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-90),\r\n $scope.validators.max(90)]],\r\n currentLng: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-180),\r\n $scope.validators.max(180)]]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"timeseries\"){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityTimeseries(\r\n datasource.entity.id,\r\n 'scope',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-location-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update location timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_multiple_attributes.json b/application/src/main/data/json/system/widget_types/update_multiple_attributes.json new file mode 100644 index 0000000000..1d3950cf65 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_multiple_attributes.json @@ -0,0 +1,22 @@ +{ + "fqn": "input_widgets.update_multiple_attributes", + "name": "Update Multiple Attributes", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Allows to create an input form and set values for multiple attributes simultaneously.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": ".tb-toast {\n min-width: 0;\n font-size: 14px !important;\n}", + "controllerScript": "self.onInit = function() {\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.multipleInputWidget.onDataUpdated();\r\n}\r\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-update-multiple-attributes-widget-settings", + "dataKeySettingsDirective": "tb-update-multiple-attributes-key-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_server_boolean_attribute.json b/application/src/main/data/json/system/widget_types/update_server_boolean_attribute.json new file mode 100644 index 0000000000..cf5d300751 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_server_boolean_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_server_boolean_attribute", + "name": "Update server boolean attribute", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAIQSURBVHja7d3PSxRhHMfx+VcWgmg3ltKD7BJshyBIQusQ3vIYCILCRMyhH6xShlGWMGq1p1gIibxFULRR1F4ixcNqdNhmUXFipWnTYvfZ/TrjJqFQ13ge3p/L8MztBQ/P8/0eZr6WNHyvrHk8vy5WoxIo0TwqqDQsPxADEviWp0yAKM8qixEpAwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC5H9DSiP2noyUNIXsc9j2qKYQ2/732lzI7LyukN4jUV7uLrtndIWcuP4lzKYBELf9rD66/bzZhqw9vFcM37ReTzz5pR3kc8fAWPriDmT58NDl5F0R51j2zOm6PpB+13VfyNNbIm8O/Iwg430iz87Lu/iaNDKz+kB6Hcd5HH60PDedjfkR5FXi2oeWyI3j+Xy+56puW2vh6KXcnTZEFq90pQripAbC5HSDDDoin9qQjxVRkwk1dUrLU2v4bLAxGFuNIEPnanK/s7WSeNDcct7rBvFOxg7djBUjSLX/YDJdEHmbiccv1PQrUb7++T/B9/Xfd8sPikbK+L81VqP7HEu0ukCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAADEUYsyAYDNGNn/zrboZQ7SVZcZYcyXbTFGWg8aBYQQAAAAASUVORK5CYII=", + "description": "Simple form to input new boolean value for pre-defined server-side attribute key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Attribute type and boolean value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\r\n
\r\n
\r\n
\r\n
\r\n \r\n {{currentValue}}\r\n \r\n
\r\n
\r\n\r\n
\r\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\r\n
\r\n
\r\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\r\n
\r\n
\r\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\r\n
\r\n
\r\n
\r\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let settings;\nlet attributeService;\nlet utils;\nlet translate;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.checkboxValue\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-boolean-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_server_date_attribute.json b/application/src/main/data/json/system/widget_types/update_server_date_attribute.json new file mode 100644 index 0000000000..3a1cdabc86 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_server_date_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_server_date_attribute", + "name": "Update server date attribute", + "deprecated": true, + "image": "data:image/png;base64,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", + "description": "Simple form to input new date value for pre-defined server-side attribute key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Attribute type and date value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ labelValue }}\n \n \n \n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n\r\n $scope.datePickerType = settings.showTimeInput ? 'datetime' : 'date'; \r\n \r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\r\n $scope.labelValue = translate.instant('widgets.input-widgets.date');\r\n \r\n if (settings.showTimeInput) {\r\n $scope.labelValue += \" & \" + translate.instant('widgets.input-widgets.time');\r\n }\r\n \r\n var validators = [];\r\n \r\n if (settings.isRequired) {\r\n validators.push($scope.validators.required);\r\n }\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, validators]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.clear = function(event) {\r\n event.stopPropagation();\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(undefined);\r\n }\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds;\r\n \r\n if (!$scope.attributeUpdateFormGroup.get('currentValue').value) {\r\n currentValueInMilliseconds = undefined;\r\n } else {\r\n currentValueInMilliseconds = $scope.attributeUpdateFormGroup.get('currentValue').value.getTime();\r\n }\r\n \r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SERVER_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: currentValueInMilliseconds\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n \r\n $scope.isValidDate = function(date) {\r\n return date instanceof Date && !isNaN(date);\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.originalValue = moment(self.ctx.data[0].data[0][1]).toDate();\r\n\r\n if (!$scope.isValidDate($scope.originalValue)) {\r\n $scope.originalValue = undefined;\r\n }\r\n \r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n}\r\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-date-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_server_double_attribute.json b/application/src/main/data/json/system/widget_types/update_server_double_attribute.json new file mode 100644 index 0000000000..ddfe7b36d1 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_server_double_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_server_double_attribute", + "name": "Update server double attribute", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAANMSURBVHja7d3fS5NRGAfw/SsrhSCKKIugburCi6KCLrropi4izME0c1kLUxquHybmKNPAi4qMJkm9hOkswhVkpaLMfhmlbjHYu+3Vaf7a3r3fLjad5nZh6NiZ3+diO+fAe/HhOc85Z+/Fjg4R2T0ieLjlMHQRT0iF4KGGPBGdHEIWREjWudVsgKhu3QiyIkYIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQghZS8jbbwDQOT9slJc/5HGJAOmoAaAUjKWERCO9z7RI5kPGC8cBxy3A5/wUBoyy1qNirg+I9jtlAPBcvGkt7xWgRmyvgKpu9JR3NF4HjHLk9DSCBmi2eqnsFwCEK8vGRSj2j9fgM85h0Ito4VgC0m/R4GwEgHZ72xMRIOHiwIv7wJ/HN6oKfAmIdL62tsoCAMps1CvE8vuwvWIIaHgehTEGmULQgPYGv9+vCLWP/Cg1a4ClG18LvDDKKPmJPgNGzwXgdQkF0S5JAPrPXqgzDcAo402R2WYAukorzUMi7uzR6YWqmY0BJzQeUQghhBBC1h9kZllDDIj2cumBaKZaijWk6hmRIFqLybl0RDJJi75EgWitJse/Y20mKf4hDkRrWe4AJJOUPB8ZC0mWj1hOUjgyDjJ9zw1Ae5rcsUKIbK9rmVzovatv+hJvfh5ec4hirfidYl6teGp1bd1/Km9XfDhasOnksZxGAIGmA/rWtU/J2NXL7tT5WEmxz+0sUqHsNsR69lwXcDc3AC13b0U6IFCsZanzsZLld1D/AYApP/5wDYBBfTfwPupPCwSKNbnj/zbE44WLOvYcLwCkCYLpVTyiODb2zDe/P7qypQnphKzioXF0myUxMw9t3+MUFBLcd2LxP1qo1bnDQkKmjhydWjqw4YGIkMiJ/MT5ufggAL9eEhCilWzudLlcLgWvbYBDfyfoPpOnCAiZ1MeiGeYdKtCcp9cfHhB11YoXeewFo2+Cv9kJIYQQQghZf5DbaQ1mhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCEkMyFZc0FwdlzZPC7rwtlxibaqy45rzVX8BUWnY6Q1/jMIAAAAAElFTkSuQmCC", + "description": "Simple form to input new double value for pre-defined server-side attribute key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Attribute type and double value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-double-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"showLabel\":true,\"isRequired\":true},\"title\":\"Update server double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_server_image_attribute.json b/application/src/main/data/json/system/widget_types/update_server_image_attribute.json new file mode 100644 index 0000000000..79af76660c --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_server_image_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_server_image_attribute", + "name": "Update server image attribute", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Simple form to input new image for pre-defined server-side attribute key.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n \n
\n \n
\n \n \n
\n
\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.tb-image-preview-container div,\n.tb-flow-drop label {\n font-size: 16px !important;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.displayPreview = utils.defaultValue(settings.displayPreview, true);\r\n settings.displayClearButton = utils.defaultValue(settings.displayClearButton, false);\r\n settings.displayApplyButton = utils.defaultValue(settings.displayApplyButton, true);\r\n settings.displayDiscardButton = utils.defaultValue(settings.displayDiscardButton, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, []]}\r\n );\r\n \r\n $scope.attributeUpdateFormGroup.valueChanges.subscribe( () => {\r\n self.ctx.detectChanges();\r\n if (!settings.displayApplyButton) {\r\n $scope.updateAttribute();\r\n }\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SERVER_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n if (settings.displayApplyButton) {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n }\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n var value = self.ctx.data[0].data[0][1];\r\n if (settings.displayApplyButton || !$scope.originalValue) {\r\n $scope.originalValue = value;\r\n }\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(value, {emitEvent: false});\r\n self.ctx.detectChanges();\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n $scope.attributeUpdateFormGroup.valueChanges.unsubscribe();\r\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-image-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"displayPreview\":true,\"displayClearButton\":false,\"displayApplyButton\":true,\"displayDiscardButton\":true},\"title\":\"Update server image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_server_integer_attribute.json b/application/src/main/data/json/system/widget_types/update_server_integer_attribute.json new file mode 100644 index 0000000000..ea4fecdb42 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_server_integer_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_server_integer_attribute", + "name": "Update server integer attribute", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAANBSURBVHja7d3dS1NhHAfw/Sum1IUQBZYJIdSF4VVQIOSNXZiJg5nmWi18CXG9WJgSqd1IBUWu10PY1EBcIFYqyoTCLOZWA8/0uGmbbjs73y7mS+Z2YdjYs76/i3Oe88C5+PB73jhwnkeHiOxyCh4uOQxdxO1XIXiofndEJ/uRBuGXdS41HSCqS+dEWoSTEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYSQfwl59xkA+teqDfLWl9wOESC9twAo5QsJIdHI6EstkvoQX4UP6LsNzNo/hgGDrI2oCI0B0XG7DADuSzcttaMC9JG2t0DTMEZqezuvAwY5UhbEvB5a213J9A0Awg0mnwid/cM1zBpCmPQgWrGwARlv1GDvBABbd88TESDhc3Ov7wM/H99oKp/dgEgXWlqaGgFAWYl6hBh+H9rqp4COV1EYYpAA5vWwdXi9XkWoeeRLjVkDGofxqdwDg4zqrxjTY+b8HDwOoSDaZQnAeNXFVuMEDDIGKs1temCwpsE8JeLMHg2u95qVGHBR4xKFEEIIIeT/gyxvKYgB0d5sXhAtN0uxgtS8LBJEsxrtm2sko/TbTRSI9tzY92ddj1FavYgD0axbHYBklOLnI2Uh8fIRy0kCR8pBgvdcALRn8R3bhMjdrdYlAFh62mpd/Uk81J8ciGKp/56gXW27aQ3uPVqac9AJTOUePpOfMw1gom5/VpJSsnC1zpU4H9vp7KEDlSqUQ3qg6GQI4cKzwIvMU6eTBYFiMSXOx3aG38mM9wCMBUDDAIAr+YDzB9qTBoFiie/4uwmxuGJ1+CgqAYBkQhDcwSVKX+YIANi6SvKmkw7ZwUXjzL5GAEDlsT2l8wJD5o+UrO1o4S0sFhcSOH4isP7wYFdAVEikpEABgGBWF4D27KigEK06u9/hcDgUlOUNLQ7lmkRtWksZsXiEhaqsjN3mgMij1lqEPcJsY8OPD4QQQgghhOws5E5SgxkhhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCElNSNocEJweRzb7ZF04PQ7RVnXpcay5il+KVmW7YpZ2rQAAAABJRU5ErkJggg==", + "description": "Simple form to input new integer value for pre-defined server attribute key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Attribute type and integer value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)\n ];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n \n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-integer-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_server_location_attribute.json b/application/src/main/data/json/system/widget_types/update_server_location_attribute.json new file mode 100644 index 0000000000..0cd032f8c1 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_server_location_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_server_location_attribute", + "name": "Update server location attribute", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Simple form to input new location for pre-defined server attribute key.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? latLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n \n \n {{ settings.showLabel ? lngLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n settings.isLatRequired = utils.defaultValue(settings.isLatRequired, true);\r\n settings.isLngRequired = utils.defaultValue(settings.isLngRequired, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n \r\n var validatorsLat = [$scope.validators.min(-90), $scope.validators.max(90)];\r\n var validatorsLng = [$scope.validators.min(-180), $scope.validators.max(180)];\r\n \r\n if (settings.isLatRequired) {\r\n validatorsLat.push($scope.validators.required);\r\n }\r\n \r\n if (settings.isLngRequired) {\r\n validatorsLng.push($scope.validators.required);\r\n }\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group({\r\n currentLat: [undefined, validatorsLat],\r\n currentLng: [undefined, validatorsLng]\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"attribute\") {\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SERVER_SCOPE',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-location-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_server_string_attribute.json b/application/src/main/data/json/system/widget_types/update_server_string_attribute.json new file mode 100644 index 0000000000..193300cb06 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_server_string_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_server_string_attribute", + "name": "Update server string attribute", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAOkSURBVHja7d3bSxRRHAfw/VdG6yklCbPsRk+GhVLYDSXMohC1tczVUkxFXDULzW4a+SA9VIZRm5haIJqBmMqW2cUoddUVR1sved2dnV/nuJaX3X0Q1pjZvj/YM4fDmYfP/uZcdvbhaMgmmnpVHibRShpb/4REKg9pot+mESfIC2JC1Jgkb4BIJk0veUX0AgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAsp6QN194+epPs1Z0vqm/Uw2QuuussMSOuYXYbe3PZJvyIeNx40T1xUTDTe+sHCK3STTfwQDGpgVU/+Vr+ox2FYyRktdEuS3UllFXVsAhtrMz9DOe5JI7htQfvIM1K3VcDYO9NZ+GtfP00Uz2uLEliDFHpqYy3qG2suaxGiDW86PVFURTD6/mxg4vQQwpRUW5ObyDZc5uVsX0+6A2s5uo9LmdtA7INIfUlo6MjFhUtY58S06TiXJa6HOsmc9aSd+pI576Lo6SuVNVEDndwErjhUs3dO85pCExrSSeqDE5K61bjSu7febvqJlzACdlbFEAAQQQQP4/yKxTRR0Q+eXKDdFsocFRMRTOqgkiP9E1rWwx6AzLLmqByE919avbajihxqVDsRCWj3rnVpYM1/lQLMRVPhw5ceNQHGTmnok7qlw7PAuJOrWOEIs+c8DNc+XxR2tdITSWd8XkPh8eHezrC2E5SXWfjzVNvxUdRn0Bf29V3sZvq2GVD82FuUYy5ukbFiBdN7MbeU+pOvvuV96xoyrT4kGJa8eaF8Sg/cHaEN9WIr/ixe9/8+7QxD0bsgITDgqPWMu2refChXIi28mApIiNzOYX7B826MEB76EtStC+SbLuSFkOiZZoaos/ewcUdYh9AodJ1gX8ovubekhO2Ms6hk8pcdMYlM+KmBPLIUWscoy1UF7Q4hjpEpopTMsq9cKYo6NCIacjV0MiY1hRELgImRQqKUBYiC5VQ4aEatqZ2MljWvGQYP6C9LAz5CirvBA+UUyYndXmSPGQ2GDjwG0fZ4hQPPh21xH2l5BP+oCoD7UrHtITIvgej3CGnIkWhAN9fELfLggh7YrNyLIQp92sWUOOq31QxG92QAABBBBAvBNy658GMgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAokyI1xwQ7B1HNo+LGqt3HKItabzjWHOJfgM5tA7UOw6xeQAAAABJRU5ErkJggg==", + "description": "Simple form to input new string value for pre-defined server-side attribute key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Attribute type and string value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n \n var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-string-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"showLabel\":true,\"isRequired\":true},\"title\":\"Update server string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_shared_boolean_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_boolean_attribute.json new file mode 100644 index 0000000000..765c6381f2 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_shared_boolean_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_shared_boolean_attribute", + "name": "Update shared boolean attribute", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAIQSURBVHja7d3PSxRhHMfx+VcWgmg3ltKD7BJshyBIQusQ3vIYCILCRMyhH6xShlGWMGq1p1gIibxFULRR1F4ixcNqdNhmUXFipWnTYvfZ/TrjJqFQ13ge3p/L8MztBQ/P8/0eZr6WNHyvrHk8vy5WoxIo0TwqqDQsPxADEviWp0yAKM8qixEpAwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC5H9DSiP2noyUNIXsc9j2qKYQ2/732lzI7LyukN4jUV7uLrtndIWcuP4lzKYBELf9rD66/bzZhqw9vFcM37ReTzz5pR3kc8fAWPriDmT58NDl5F0R51j2zOm6PpB+13VfyNNbIm8O/Iwg430iz87Lu/iaNDKz+kB6Hcd5HH60PDedjfkR5FXi2oeWyI3j+Xy+56puW2vh6KXcnTZEFq90pQripAbC5HSDDDoin9qQjxVRkwk1dUrLU2v4bLAxGFuNIEPnanK/s7WSeNDcct7rBvFOxg7djBUjSLX/YDJdEHmbiccv1PQrUb7++T/B9/Xfd8sPikbK+L81VqP7HEu0ukCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAADEUYsyAYDNGNn/zrboZQ7SVZcZYcyXbTFGWg8aBYQQAAAAASUVORK5CYII=", + "description": "Simple form to input new boolean value for pre-defined shared attribute key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Attribute type and boolean value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\r\n
\r\n
\r\n
\r\n
\r\n \r\n {{currentValue}}\r\n \r\n
\r\n
\r\n\r\n
\r\n
\r\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\r\n
\r\n
\r\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\r\n
\r\n
\r\n
\r\n
", + "templateCss": ".attribute-update-form {\r\n overflow: hidden;\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n display: flex;\r\n}\r\n.grid__element:first-child {\r\n flex: 1;\r\n}\r\n\r\n.grid__element {\r\n display: flex;\r\n}\r\n\r\n.attribute-update-form .mat-button.mat-icon-button {\r\n width: 32px;\r\n min-width: 32px;\r\n height: 32px;\r\n min-height: 32px;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .mat-icon-button mat-icon {\r\n width: 20px;\r\n min-width: 20px;\r\n height: 20px;\r\n min-height: 20px;\r\n font-size: 20px;\r\n}\r\n\r\n.tb-toast {\r\n font-size: 14px!important;\r\n}", + "controllerScript": "let settings;\nlet attributeService;\nlet utils;\nlet translate;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.checkboxValue || false\n }\n ]\n ).subscribe(\n function success() {\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-boolean-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_shared_date_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_date_attribute.json new file mode 100644 index 0000000000..2225111ff7 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_shared_date_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_shared_date_attribute", + "name": "Update shared date attribute", + "deprecated": true, + "image": "data:image/png;base64,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", + "description": "Simple form to input new date for pre-defined shared attribute key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Attribute type and date value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ labelValue }}\n \n \n \n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n\r\n $scope.datePickerType = settings.showTimeInput ? 'datetime' : 'date'; \r\n \r\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\r\n $scope.labelValue = translate.instant('widgets.input-widgets.date');\r\n \r\n \r\n if (settings.showTimeInput) {\r\n $scope.labelValue += \" & \" + translate.instant('widgets.input-widgets.time');\r\n }\r\n \r\n var validators = [];\r\n \r\n if (settings.isRequired) {\r\n validators.push($scope.validators.required);\r\n }\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, validators]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType === 'DEVICE') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.clear = function(event) {\r\n event.stopPropagation();\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(undefined);\r\n }\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds;\r\n \r\n if (!$scope.attributeUpdateFormGroup.get('currentValue').value) {\r\n currentValueInMilliseconds = undefined;\r\n } else {\r\n currentValueInMilliseconds = $scope.attributeUpdateFormGroup.get('currentValue').value.getTime();\r\n }\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SHARED_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: currentValueInMilliseconds\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n \r\n $scope.isValidDate = function(date) {\r\n return date instanceof Date && !isNaN(date);\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.originalValue = moment(self.ctx.data[0].data[0][1]).toDate();\r\n \r\n if (!$scope.isValidDate($scope.originalValue)) {\r\n $scope.originalValue = undefined;\r\n }\r\n \r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-date-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"isRequired\":true,\"showLabel\":true,\"showTimeInput\":true},\"title\":\"Update shared date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_shared_double_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_double_attribute.json new file mode 100644 index 0000000000..29c96f71b4 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_shared_double_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_shared_double_attribute", + "name": "Update shared double attribute", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAANMSURBVHja7d3fS5NRGAfw/SsrhSCKKIugburCi6KCLrropi4izME0c1kLUxquHybmKNPAi4qMJkm9hOkswhVkpaLMfhmlbjHYu+3Vaf7a3r3fLjad5nZh6NiZ3+diO+fAe/HhOc85Z+/Fjg4R2T0ieLjlMHQRT0iF4KGGPBGdHEIWREjWudVsgKhu3QiyIkYIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQghZS8jbbwDQOT9slJc/5HGJAOmoAaAUjKWERCO9z7RI5kPGC8cBxy3A5/wUBoyy1qNirg+I9jtlAPBcvGkt7xWgRmyvgKpu9JR3NF4HjHLk9DSCBmi2eqnsFwCEK8vGRSj2j9fgM85h0Ito4VgC0m/R4GwEgHZ72xMRIOHiwIv7wJ/HN6oKfAmIdL62tsoCAMps1CvE8vuwvWIIaHgehTEGmULQgPYGv9+vCLWP/Cg1a4ClG18LvDDKKPmJPgNGzwXgdQkF0S5JAPrPXqgzDcAo402R2WYAukorzUMi7uzR6YWqmY0BJzQeUQghhBBC1h9kZllDDIj2cumBaKZaijWk6hmRIFqLybl0RDJJi75EgWitJse/Y20mKf4hDkRrWe4AJJOUPB8ZC0mWj1hOUjgyDjJ9zw1Ae5rcsUKIbK9rmVzovatv+hJvfh5ec4hirfidYl6teGp1bd1/Km9XfDhasOnksZxGAIGmA/rWtU/J2NXL7tT5WEmxz+0sUqHsNsR69lwXcDc3AC13b0U6IFCsZanzsZLld1D/AYApP/5wDYBBfTfwPupPCwSKNbnj/zbE44WLOvYcLwCkCYLpVTyiODb2zDe/P7qypQnphKzioXF0myUxMw9t3+MUFBLcd2LxP1qo1bnDQkKmjhydWjqw4YGIkMiJ/MT5ufggAL9eEhCilWzudLlcLgWvbYBDfyfoPpOnCAiZ1MeiGeYdKtCcp9cfHhB11YoXeewFo2+Cv9kJIYQQQghZf5DbaQ1mhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCEkMyFZc0FwdlzZPC7rwtlxibaqy45rzVX8BUWnY6Q1/jMIAAAAAElFTkSuQmCC", + "description": "Simple form to input new double value for pre-defined shared attribute key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Attribute type and double value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)\n ];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n \n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-double-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_shared_image_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_image_attribute.json new file mode 100644 index 0000000000..895ef845c9 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_shared_image_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_shared_image_attribute", + "name": "Update shared image attribute", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Simple form to input new image for pre-defined shared attribute key.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.tb-image-preview-container div,\n.tb-flow-drop label {\n font-size: 16px !important;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.displayPreview = utils.defaultValue(settings.displayPreview, true);\r\n settings.displayClearButton = utils.defaultValue(settings.displayClearButton, false);\r\n settings.displayApplyButton = utils.defaultValue(settings.displayApplyButton, true);\r\n settings.displayDiscardButton = utils.defaultValue(settings.displayDiscardButton, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, []]}\r\n );\r\n \r\n $scope.attributeUpdateFormGroup.valueChanges.subscribe( () => {\r\n self.ctx.detectChanges();\r\n if (!settings.displayApplyButton) {\r\n $scope.updateAttribute();\r\n }\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType === 'DEVICE') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SHARED_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n if (settings.displayApplyButton) {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n }\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n var value = self.ctx.data[0].data[0][1];\r\n if (settings.displayApplyButton || !$scope.originalValue) {\r\n $scope.originalValue = value;\r\n }\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(value, {emitEvent: false});\r\n self.ctx.detectChanges();\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n $scope.attributeUpdateFormGroup.valueChanges.unsubscribe();\r\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-image-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"displayPreview\":true,\"displayClearButton\":false,\"displayApplyButton\":true,\"displayDiscardButton\":true},\"title\":\"Update shared image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_shared_integer_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_integer_attribute.json new file mode 100644 index 0000000000..c4dd5b2dba --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_shared_integer_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_shared_integer_attribute", + "name": "Update shared integer attribute", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAANBSURBVHja7d3dS1NhHAfw/Sum1IUQBZYJIdSF4VVQIOSNXZiJg5nmWi18CXG9WJgSqd1IBUWu10PY1EBcIFYqyoTCLOZWA8/0uGmbbjs73y7mS+Z2YdjYs76/i3Oe88C5+PB73jhwnkeHiOxyCh4uOQxdxO1XIXiofndEJ/uRBuGXdS41HSCqS+dEWoSTEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYSQfwl59xkA+teqDfLWl9wOESC9twAo5QsJIdHI6EstkvoQX4UP6LsNzNo/hgGDrI2oCI0B0XG7DADuSzcttaMC9JG2t0DTMEZqezuvAwY5UhbEvB5a213J9A0Awg0mnwid/cM1zBpCmPQgWrGwARlv1GDvBABbd88TESDhc3Ov7wM/H99oKp/dgEgXWlqaGgFAWYl6hBh+H9rqp4COV1EYYpAA5vWwdXi9XkWoeeRLjVkDGofxqdwDg4zqrxjTY+b8HDwOoSDaZQnAeNXFVuMEDDIGKs1temCwpsE8JeLMHg2u95qVGHBR4xKFEEIIIeT/gyxvKYgB0d5sXhAtN0uxgtS8LBJEsxrtm2sko/TbTRSI9tzY92ddj1FavYgD0axbHYBklOLnI2Uh8fIRy0kCR8pBgvdcALRn8R3bhMjdrdYlAFh62mpd/Uk81J8ciGKp/56gXW27aQ3uPVqac9AJTOUePpOfMw1gom5/VpJSsnC1zpU4H9vp7KEDlSqUQ3qg6GQI4cKzwIvMU6eTBYFiMSXOx3aG38mM9wCMBUDDAIAr+YDzB9qTBoFiie/4uwmxuGJ1+CgqAYBkQhDcwSVKX+YIANi6SvKmkw7ZwUXjzL5GAEDlsT2l8wJD5o+UrO1o4S0sFhcSOH4isP7wYFdAVEikpEABgGBWF4D27KigEK06u9/hcDgUlOUNLQ7lmkRtWksZsXiEhaqsjN3mgMij1lqEPcJsY8OPD4QQQgghhOws5E5SgxkhhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCElNSNocEJweRzb7ZF04PQ7RVnXpcay5il+KVmW7YpZ2rQAAAABJRU5ErkJggg==", + "description": "Simple form to input new integer value for pre-defined shared attribute key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Attribute type and integer value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)\n ];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-integer-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_shared_location_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_location_attribute.json new file mode 100644 index 0000000000..7df35d28a6 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_shared_location_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_shared_location_attribute", + "name": "Update shared location attribute", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Simple form to input new location for pre-defined shared attribute key.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? latLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n \n \n {{ settings.showLabel ? lngLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n settings.isLatRequired = utils.defaultValue(settings.isLatRequired, true);\r\n settings.isLngRequired = utils.defaultValue(settings.isLngRequired, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n\r\n var validatorsLat = [$scope.validators.min(-90), $scope.validators.max(90)];\r\n var validatorsLng = [$scope.validators.min(-180), $scope.validators.max(180)];\r\n \r\n if (settings.isLatRequired) {\r\n validatorsLat.push($scope.validators.required);\r\n }\r\n \r\n if (settings.isLngRequired) {\r\n validatorsLng.push($scope.validators.required);\r\n }\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group({\r\n currentLat: [undefined, validatorsLat],\r\n currentLng: [undefined, validatorsLng],\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType === 'DEVICE') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"attribute\"){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SHARED_SCOPE',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-location-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_shared_string_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_string_attribute.json new file mode 100644 index 0000000000..4c5333ec59 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_shared_string_attribute.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_shared_string_attribute", + "name": "Update shared string attribute", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAOkSURBVHja7d3bSxRRHAfw/VdG6yklCbPsRk+GhVLYDSXMohC1tczVUkxFXDULzW4a+SA9VIZRm5haIJqBmMqW2cUoddUVR1sved2dnV/nuJaX3X0Q1pjZvj/YM4fDmYfP/uZcdvbhaMgmmnpVHibRShpb/4REKg9pot+mESfIC2JC1Jgkb4BIJk0veUX0AgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAsp6QN194+epPs1Z0vqm/Uw2QuuussMSOuYXYbe3PZJvyIeNx40T1xUTDTe+sHCK3STTfwQDGpgVU/+Vr+ox2FYyRktdEuS3UllFXVsAhtrMz9DOe5JI7htQfvIM1K3VcDYO9NZ+GtfP00Uz2uLEliDFHpqYy3qG2suaxGiDW86PVFURTD6/mxg4vQQwpRUW5ObyDZc5uVsX0+6A2s5uo9LmdtA7INIfUlo6MjFhUtY58S06TiXJa6HOsmc9aSd+pI576Lo6SuVNVEDndwErjhUs3dO85pCExrSSeqDE5K61bjSu7febvqJlzACdlbFEAAQQQQP4/yKxTRR0Q+eXKDdFsocFRMRTOqgkiP9E1rWwx6AzLLmqByE919avbajihxqVDsRCWj3rnVpYM1/lQLMRVPhw5ceNQHGTmnok7qlw7PAuJOrWOEIs+c8DNc+XxR2tdITSWd8XkPh8eHezrC2E5SXWfjzVNvxUdRn0Bf29V3sZvq2GVD82FuUYy5ukbFiBdN7MbeU+pOvvuV96xoyrT4kGJa8eaF8Sg/cHaEN9WIr/ixe9/8+7QxD0bsgITDgqPWMu2refChXIi28mApIiNzOYX7B826MEB76EtStC+SbLuSFkOiZZoaos/ewcUdYh9AodJ1gX8ovubekhO2Ms6hk8pcdMYlM+KmBPLIUWscoy1UF7Q4hjpEpopTMsq9cKYo6NCIacjV0MiY1hRELgImRQqKUBYiC5VQ4aEatqZ2MljWvGQYP6C9LAz5CirvBA+UUyYndXmSPGQ2GDjwG0fZ4hQPPh21xH2l5BP+oCoD7UrHtITIvgej3CGnIkWhAN9fELfLggh7YrNyLIQp92sWUOOq31QxG92QAABBBBAvBNy658GMgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAokyI1xwQ7B1HNo+LGqt3HKItabzjWHOJfgM5tA7UOw6xeQAAAABJRU5ErkJggg==", + "description": "Simple form to input new string value for pre-defined shared attribute key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Attribute type and string value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-string-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/update_string_timeseries.json b/application/src/main/data/json/system/widget_types/update_string_timeseries.json new file mode 100644 index 0000000000..43a96d6f69 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/update_string_timeseries.json @@ -0,0 +1,21 @@ +{ + "fqn": "input_widgets.update_string_timeseries", + "name": "Update string timeseries", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAOkSURBVHja7d3bSxRRHAfw/VdG6yklCbPsRk+GhVLYDSXMohC1tczVUkxFXDULzW4a+SA9VIZRm5haIJqBmMqW2cUoddUVR1sved2dnV/nuJaX3X0Q1pjZvj/YM4fDmYfP/uZcdvbhaMgmmnpVHibRShpb/4REKg9pot+mESfIC2JC1Jgkb4BIJk0veUX0AgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAsp6QN194+epPs1Z0vqm/Uw2QuuussMSOuYXYbe3PZJvyIeNx40T1xUTDTe+sHCK3STTfwQDGpgVU/+Vr+ox2FYyRktdEuS3UllFXVsAhtrMz9DOe5JI7htQfvIM1K3VcDYO9NZ+GtfP00Uz2uLEliDFHpqYy3qG2suaxGiDW86PVFURTD6/mxg4vQQwpRUW5ObyDZc5uVsX0+6A2s5uo9LmdtA7INIfUlo6MjFhUtY58S06TiXJa6HOsmc9aSd+pI576Lo6SuVNVEDndwErjhUs3dO85pCExrSSeqDE5K61bjSu7febvqJlzACdlbFEAAQQQQP4/yKxTRR0Q+eXKDdFsocFRMRTOqgkiP9E1rWwx6AzLLmqByE919avbajihxqVDsRCWj3rnVpYM1/lQLMRVPhw5ceNQHGTmnok7qlw7PAuJOrWOEIs+c8DNc+XxR2tdITSWd8XkPh8eHezrC2E5SXWfjzVNvxUdRn0Bf29V3sZvq2GVD82FuUYy5ukbFiBdN7MbeU+pOvvuV96xoyrT4kGJa8eaF8Sg/cHaEN9WIr/ixe9/8+7QxD0bsgITDgqPWMu2refChXIi28mApIiNzOYX7B826MEB76EtStC+SbLuSFkOiZZoaos/ewcUdYh9AodJ1gX8ovubekhO2Ms6hk8pcdMYlM+KmBPLIUWscoy1UF7Q4hjpEpopTMsq9cKYo6NCIacjV0MiY1hRELgImRQqKUBYiC5VQ4aEatqZ2MljWvGQYP6C9LAz5CirvBA+UUyYndXmSPGQ2GDjwG0fZ4hQPPh21xH2l5BP+oCoD7UrHtITIvgej3CGnIkWhAN9fELfLggh7YrNyLIQp92sWUOOq31QxG92QAABBBBAvBNy658GMgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAokyI1xwQ7B1HNo+LGqt3HKItabzjWHOJfgM5tA7UOw6xeQAAAABJRU5ErkJggg==", + "description": "Simple form to input new string value for pre-defined timeseries key.\nThe widget is deprecated. Use \"Update Multiple Attributes\" widget. Timeseries key type and string value type can be selected in widgets data key configuration.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-string-attribute-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update string timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/usage_info.json b/application/src/main/data/json/system/widget_types/usage_info.json new file mode 100644 index 0000000000..0e936b1c60 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/usage_info.json @@ -0,0 +1,21 @@ +{ + "fqn": "home_page_widgets.usage_info", + "name": "Usage info", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Display statistics about platform entities and API usage.", + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Usage info\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/value_and_chart_card.json b/application/src/main/data/json/system/widget_types/value_and_chart_card.json new file mode 100644 index 0000000000..136419c70c --- /dev/null +++ b/application/src/main/data/json/system/widget_types/value_and_chart_card.json @@ -0,0 +1,26 @@ +{ + "fqn": "cards.aggregated_value_card", + "name": "Value and chart card", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays current and aggregated difference values with a timeseries chart. Widget styles are customizable.", + "descriptor": { + "type": "timeseries", + "sizeX": 4.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n\n", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.aggregatedValueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.aggregatedValueCardWidget.onDataUpdated();\n};\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.aggregatedValueCardWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.aggregatedValueCardWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.aggregatedValueCardWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.aggregatedValueCardWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '400px',\n previewHeight: '300px',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}", + "latestDataKeySettingsSchema": "{}", + "settingsDirective": "tb-aggregated-value-card-widget-settings", + "dataKeySettingsDirective": "", + "latestDataKeySettingsDirective": "tb-aggregated-value-card-key-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-aggregated-value-card-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"Main building\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"watermeter\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 80) {\\n\\tvalue = 80;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":\"m³\",\"decimals\":0,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"#4caf50\",\"settings\":{\"position\":\"center\",\"font\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"1\"},\"color\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[],\"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';\"},\"showArrow\":false},\"_hash\":0.9408410830697858,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 80) {\\n\\tvalue = 80;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":\"m³\",\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Delta percent\",\"color\":\"#f44336\",\"settings\":{\"position\":\"rightTop\",\"font\":{\"size\":14,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"20px\"},\"color\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"rangeList\":[{\"from\":null,\"to\":0,\"color\":\"#198038\"},{\"from\":0,\"to\":0,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0,\"to\":null,\"color\":\"#D12730\"}],\"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';\"},\"showArrow\":true},\"_hash\":0.06392321853157967,\"funcBody\":\"var value = prevValue + Math.random() * 6 - 3;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -25) {\\n\\tvalue = -25;\\n} else if (value > 25) {\\n\\tvalue = 25;\\n} \\nreturn value;\",\"aggregationType\":null,\"units\":\"%\",\"decimals\":0,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Delta absolute\",\"color\":\"#607d8b\",\"settings\":{\"position\":\"rightBottom\",\"font\":{\"size\":11,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"rangeList\":[],\"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';\"},\"showArrow\":false},\"_hash\":0.44695098620509865,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":\"m³\",\"decimals\":1,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1691927717318,\"endTimeMs\":1692014117318},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"showSubtitle\":true,\"subtitle\":\"${entityName}\",\"subtitleFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"subtitleColor\":\"rgba(0, 0, 0, 0.38)\",\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"dateColor\":\"rgba(0, 0, 0, 0.38)\",\"showChart\":true,\"chartColor\":\"rgba(0, 0, 0, 0.87)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Value and chart card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"water_drop\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"decimals\":0,\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":false},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/value_card.json b/application/src/main/data/json/system/widget_types/value_card.json new file mode 100644 index 0000000000..bfab0cbac1 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/value_card.json @@ -0,0 +1,23 @@ +{ + "fqn": "cards.value_card", + "name": "Value card", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Designed to display single value of the selected attribute or timeseries data. Widget styles are customizable.", + "descriptor": { + "type": "latest", + "sizeX": 3, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};\n\nself.onDestroy = function() {\n};\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-value-card-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-value-card-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"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';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"constant\",\"color\":\"#5469FF\",\"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';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"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';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"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';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/vertical_bar.json b/application/src/main/data/json/system/widget_types/vertical_bar.json new file mode 100644 index 0000000000..6f658d91a9 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/vertical_bar.json @@ -0,0 +1,21 @@ +{ + "fqn": "digital_gauges.vertical_bar_justgage", + "name": "Vertical bar", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABQVBMVEVqampra2tsbGxtbW1ubm5vb29wcHBycnJzc3N0dHR2dnZ3d3d5eXl6enp7e3t8fHx+fn6BgYGDg4OFhYWHh4eIiIiKioqNjY2Ojo6RkZGTk5OWlpaZmZmbm5uenp6fn5+goKCkpKSlpaWmpqanp6eoqKiqqqqrq6usrKytra2xsbGysrK0tLS1tbW2tra4uLi6urq7u7u9vb2+vr7AwMDBwcHExMTFxcXHx8fIyMjJycnKysrMzMzNzc3Ozs7Pz8/Q0NDR0dHS0tLT09PU1NTV1dXX19fY2NjZ2dnb29vc3Nzd3d3f39/g4ODh4eHj4+Pk5OTl5eXm5ubn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fHy8vLz8/P09PT1fAD19fX29vb39/f6+vr7+/v8/Pz9/f3+/v7///8uFkw3AAAAAWJLR0RqJWKVDgAAAkBJREFUeNrt3ElTE3EQhvGebKMIiUExGkSNuCEiKAqKRKJsIuISN0giwQFC5v3+H8BD5uDBE2WV9tTTp1x/lTyd/6lNKRkDAgQIECBA/jtIHEXx4FOv3fMMWTSLJOn9eN4+OoZ0zg4gjZxduvXDMWQim7FIaoe5hutGXtn90CJp2mZcx340cv44tEgas9kLufKyW8ht21BokVSwTPWKBdtOIdvBDSm0SH2zFWnaxn1CeqPB03o9b88+9C2IpV0r+YTsWDJ3NWQdqWNDPiGH9Xp98I3ous1Ji1b1/NYKLZI+FzKTd3LBG/cQbZXNii9T8frttGKe8UCAAJGaS5J6j2/O96TuzOTz2CfkxZhVJF0tTY1MqF++eK/w0Cdkar5akdrBa60EnTXb04NzXn9a1yrSln1V094+yUvLduQYsmp7+m5rs2ekhrUdQ9ZtV99sYy6UGrbvGPLJdvTOmkvBsRay8T+CtE45v0NOirVurdRvZR/tX67KMUSbwza8KS2ENvrFKySZA0lSfMgTBUjqIH+pESBAiB0IjQBhawEBQuxAaAQIWwsIEGIHQiNA2FpAgBA7EBoBwtYCAoTYgdAIELYWECDEDoRGgKQK8vOUAwQIsQOhESBsLWIHQuxAaAQIW4vYgRA7EBoBwtYidiDEDoRGgLC1iB0IsQOhESBsrXTHnlws9g8ZXCz230hysdg/JLlY7B+SXCz230hysdg/JLlY7B9yUqx1a8V+CtZvcrE4Df/sBzxRgAABAgQIECB/mF+DKtRemhqPQgAAAABJRU5ErkJggg==", + "description": "Preconfigured gauge to display any value reading as a bar. Allows to configure value range, gradient colors and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 2, + "sizeY": 3.5, + "resources": [], + "templateHtml": "", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-digital-gauge-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":12,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":1.5,\"gaugeColor\":\"#eeeeee\",\"showTitle\":false,\"gaugeType\":\"verticalBar\"},\"title\":\"Vertical bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null +} \ No newline at end of file diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json index 4776ef2aae..7d1deb7e41 100644 --- a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json +++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json @@ -33,7 +33,11 @@ "name": "Save Client Attributes", "debugMode": false, "configuration": { - "scope": "CLIENT_SCOPE" + "version": 1, + "scope": "CLIENT_SCOPE", + "notifyDevice": "false", + "sendAttributesUpdatedNotification": "false", + "updateAttributesOnlyOnValueChange": "true" } }, { diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json index 88ef27e2f8..57c3a86dfb 100644 --- a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json +++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json @@ -32,8 +32,11 @@ "name": "Save Client Attributes", "debugMode": false, "configuration": { + "version": 1, "scope": "CLIENT_SCOPE", - "notifyDevice": "false" + "notifyDevice": "false", + "sendAttributesUpdatedNotification": "false", + "updateAttributesOnlyOnValueChange": "true" } }, { diff --git a/application/src/main/data/upgrade/3.5.1/schema_update.sql b/application/src/main/data/upgrade/3.5.1/schema_update.sql index 1655ecb978..c8efd4390e 100644 --- a/application/src/main/data/upgrade/3.5.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.5.1/schema_update.sql @@ -99,19 +99,22 @@ DECLARE p RECORD; partition_end_ts BIGINT; BEGIN - FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_edge_event - WHERE created_time >= start_time_ms AND created_time < end_time_ms - LOOP - partition_end_ts = p.partition_ts + partition_size_ms; - RAISE NOTICE '[edge_event] Partition to create : [%-%]', p.partition_ts, partition_end_ts; - EXECUTE format('CREATE TABLE IF NOT EXISTS edge_event_%s PARTITION OF edge_event ' || - 'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts); - END LOOP; - - INSERT INTO edge_event (id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts) - SELECT id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts - FROM old_edge_event - WHERE created_time >= start_time_ms AND created_time < end_time_ms; + IF (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_edge_event')) THEN + FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_edge_event + WHERE created_time >= start_time_ms AND created_time < end_time_ms + LOOP + partition_end_ts = p.partition_ts + partition_size_ms; + RAISE NOTICE '[edge_event] Partition to create : [%-%]', p.partition_ts, partition_end_ts; + EXECUTE format('CREATE TABLE IF NOT EXISTS edge_event_%s PARTITION OF edge_event ' || + 'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts); + END LOOP; + INSERT INTO edge_event (id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts) + SELECT id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts + FROM old_edge_event + WHERE created_time >= start_time_ms AND created_time < end_time_ms; + ELSE + RAISE NOTICE 'Table old_edge_event does not exists, skipping migration'; + END IF; END; $$; -- EDGE EVENTS MIGRATION END @@ -122,3 +125,67 @@ ALTER TABLE resource UPDATE resource SET etag = encode(sha256(decode(resource.data, 'base64')),'hex') WHERE resource.data is not null; +ALTER TABLE notification_request ALTER COLUMN info SET DATA TYPE varchar(1000000); + +CREATE TABLE IF NOT EXISTS alarm_types ( + tenant_id uuid NOT NULL, + type varchar(255) NOT NULL, + CONSTRAINT tenant_id_type_unq_key UNIQUE (tenant_id, type), + CONSTRAINT fk_entity_tenant_id FOREIGN KEY (tenant_id) REFERENCES tenant(id) ON DELETE CASCADE +); + +INSERT INTO alarm_types (tenant_id, type) SELECT DISTINCT tenant_id, type FROM alarm ON CONFLICT (tenant_id, type) DO NOTHING; + +ALTER TABLE widget_type + ADD COLUMN IF NOT EXISTS fqn varchar(512); +ALTER TABLE widget_type + ADD COLUMN IF NOT EXISTS deprecated boolean NOT NULL DEFAULT false; +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'uq_widget_type_fqn') THEN + UPDATE widget_type SET fqn = concat(widget_type.bundle_alias, '.', widget_type.alias); + ALTER TABLE widget_type ADD CONSTRAINT uq_widget_type_fqn UNIQUE (tenant_id, fqn); + ALTER TABLE widget_type DROP COLUMN IF EXISTS alias; + END IF; + END; +$$; + +ALTER TABLE widget_type + ADD COLUMN IF NOT EXISTS external_id UUID; +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'widget_type_external_id_unq_key') THEN + ALTER TABLE widget_type ADD CONSTRAINT widget_type_external_id_unq_key UNIQUE (tenant_id, external_id); + END IF; + END; +$$; + +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'uq_widgets_bundle_alias') THEN + ALTER TABLE widgets_bundle ADD CONSTRAINT uq_widgets_bundle_alias UNIQUE (tenant_id, alias); + END IF; + END; +$$; + +CREATE TABLE IF NOT EXISTS widgets_bundle_widget ( + widgets_bundle_id uuid NOT NULL, + widget_type_id uuid NOT NULL, + widget_type_order int NOT NULL DEFAULT 0, + CONSTRAINT widgets_bundle_widget_pkey PRIMARY KEY (widgets_bundle_id, widget_type_id), + CONSTRAINT fk_widgets_bundle FOREIGN KEY (widgets_bundle_id) REFERENCES widgets_bundle(id) ON DELETE CASCADE, + CONSTRAINT fk_widget_type FOREIGN KEY (widget_type_id) REFERENCES widget_type(id) ON DELETE CASCADE +); + +DO +$$ + BEGIN + IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'widget_type' and column_name='bundle_alias') THEN + INSERT INTO widgets_bundle_widget SELECT wb.id as widgets_bundle_id, wt.id as widget_type_id from widget_type wt left join widgets_bundle wb ON wt.bundle_alias = wb.alias ON CONFLICT (widgets_bundle_id, widget_type_id) DO NOTHING; + ALTER TABLE widget_type DROP COLUMN IF EXISTS bundle_alias; + END IF; + END; +$$; diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index d6f1385732..3d4340f6c9 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; @@ -90,7 +91,6 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.queue.discovery.DiscoveryService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.component.ComponentDiscoveryService; @@ -246,6 +246,7 @@ public class ActorSystemContext { private RuleNodeStateService ruleNodeStateService; @Autowired + @Getter private PartitionService partitionService; @Autowired diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index 1461654216..98e0bf7c25 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -46,6 +46,7 @@ import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import java.util.HashSet; +import java.util.Optional; import java.util.Set; @Slf4j @@ -129,8 +130,11 @@ public class AppActor extends ContextAwareActor { PageDataIterable tenantIterator = new PageDataIterable<>(tenantService::findTenants, ENTITY_PACK_LIMIT); for (Tenant tenant : tenantIterator) { log.debug("[{}] Creating tenant actor", tenant.getId()); - getOrCreateTenantActor(tenant.getId()); - log.debug("[{}] Tenant actor created.", tenant.getId()); + getOrCreateTenantActor(tenant.getId()).ifPresentOrElse(tenantActor -> { + log.debug("[{}] Tenant actor created.", tenant.getId()); + }, () -> { + log.debug("[{}] Skipped actor creation", tenant.getId()); + }); } } log.info("Main system actor started."); @@ -143,11 +147,9 @@ public class AppActor extends ContextAwareActor { if (TenantId.SYS_TENANT_ID.equals(msg.getTenantId())) { msg.getMsg().getCallback().onFailure(new RuleEngineException("Message has system tenant id!")); } else { - if (!deletedTenants.contains(msg.getTenantId())) { - getOrCreateTenantActor(msg.getTenantId()).tell(msg); - } else { - msg.getMsg().getCallback().onSuccess(); - } + getOrCreateTenantActor(msg.getTenantId()).ifPresentOrElse(actor -> { + actor.tell(msg); + }, () -> msg.getMsg().getCallback().onSuccess()); } } @@ -164,12 +166,13 @@ public class AppActor extends ContextAwareActor { log.info("[{}] Handling tenant deleted notification: {}", msg.getTenantId(), msg); deletedTenants.add(tenantId); ctx.stop(new TbEntityActorId(tenantId)); - } else { - target = getOrCreateTenantActor(msg.getTenantId()); + return; } - } else { - target = getOrCreateTenantActor(msg.getTenantId()); } + target = getOrCreateTenantActor(msg.getTenantId()).orElseGet(() -> { + log.debug("Ignoring component lifecycle msg for tenant {} because it is not managed by this service", msg.getTenantId()); + return null; + }); } if (target != null) { target.tellWithHighPriority(msg); @@ -179,24 +182,28 @@ public class AppActor extends ContextAwareActor { } private void onToDeviceActorMsg(TenantAwareMsg msg, boolean priority) { - if (!deletedTenants.contains(msg.getTenantId())) { - TbActorRef tenantActor = getOrCreateTenantActor(msg.getTenantId()); + getOrCreateTenantActor(msg.getTenantId()).ifPresentOrElse(tenantActor -> { if (priority) { tenantActor.tellWithHighPriority(msg); } else { tenantActor.tell(msg); } - } else { + }, () -> { if (msg instanceof TransportToDeviceActorMsgWrapper) { ((TransportToDeviceActorMsgWrapper) msg).getCallback().onSuccess(); } - } + }); } - private TbActorRef getOrCreateTenantActor(TenantId tenantId) { - return ctx.getOrCreateChildActor(new TbEntityActorId(tenantId), + private Optional getOrCreateTenantActor(TenantId tenantId) { + if (deletedTenants.contains(tenantId)) { + return Optional.empty(); + } + return Optional.ofNullable(ctx.getOrCreateChildActor(new TbEntityActorId(tenantId), () -> DefaultActorService.TENANT_DISPATCHER_NAME, - () -> new TenantActor.ActorCreator(systemContext, tenantId)); + () -> new TenantActor.ActorCreator(systemContext, tenantId), + () -> systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE) || + systemContext.getPartitionService().isManagedByCurrentService(tenantId))); } private void onToEdgeSessionMsg(EdgeSessionMsg msg) { @@ -204,7 +211,7 @@ public class AppActor extends ContextAwareActor { if (ModelConstants.SYSTEM_TENANT.equals(msg.getTenantId())) { log.warn("Message has system tenant id: {}", msg); } else { - target = getOrCreateTenantActor(msg.getTenantId()); + target = getOrCreateTenantActor(msg.getTenantId()).orElse(null); } if (target != null) { target.tellWithHighPriority(msg); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 30d909cd5a..ca1d174322 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.actors.ruleChain; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.netty.channel.EventLoopGroup; @@ -35,7 +34,6 @@ import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.util.TenantIdLoader; @@ -43,7 +41,6 @@ import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; @@ -62,6 +59,8 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleNode; @@ -115,6 +114,10 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_CREATED; + /** * Created by ashvayka on 19.03.18. */ @@ -133,7 +136,7 @@ class DefaultTbContext implements TbContext { @Override public void tellSuccess(TbMsg msg) { - tellNext(msg, Collections.singleton(TbRelationTypes.SUCCESS), null); + tellNext(msg, Collections.singleton(TbNodeConnectionType.SUCCESS), null); } @Override @@ -212,13 +215,13 @@ class DefaultTbContext implements TbContext { @Override public void enqueueForTellFailure(TbMsg tbMsg, String failureMessage) { TopicPartitionInfo tpi = resolvePartition(tbMsg); - enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbRelationTypes.FAILURE), failureMessage, null, null); + enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbNodeConnectionType.FAILURE), failureMessage, null, null); } @Override public void enqueueForTellFailure(TbMsg tbMsg, Throwable th) { TopicPartitionInfo tpi = resolvePartition(tbMsg); - enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbRelationTypes.FAILURE), getFailureMessage(th), null, null); + enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbNodeConnectionType.FAILURE), getFailureMessage(th), null, null); } @Override @@ -316,11 +319,11 @@ class DefaultTbContext implements TbContext { @Override public void tellFailure(TbMsg msg, Throwable th) { if (nodeCtx.getSelf().isDebugMode()) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbRelationTypes.FAILURE, th); + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbNodeConnectionType.FAILURE, th); } String failureMessage = getFailureMessage(th); nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(), - nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE), + nodeCtx.getSelf().getId(), Collections.singleton(TbNodeConnectionType.FAILURE), msg, failureMessage)); } @@ -343,67 +346,104 @@ class DefaultTbContext implements TbContext { return TbMsg.transformMsg(origMsg, type, originator, metaData, data); } + @Override + public TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { + return newMsg(queueName, type, originator, null, metaData, data); + } + + @Override + public TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { + return TbMsg.newMsg(queueName, type, originator, customerId, metaData, data, nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId()); + } + + @Override + public TbMsg transformMsg(TbMsg origMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { + return TbMsg.transformMsg(origMsg, type, originator, metaData, data); + } + + @Override + public TbMsg transformMsg(TbMsg origMsg, TbMsgMetaData metaData, String data) { + return TbMsg.transformMsg(origMsg, metaData, data); + } + + @Override + public TbMsg transformMsgOriginator(TbMsg origMsg, EntityId originator) { + return TbMsg.transformMsgOriginator(origMsg, originator); + } + + @Override public TbMsg customerCreatedMsg(Customer customer, RuleNodeId ruleNodeId) { - return entityActionMsg(customer, customer.getId(), ruleNodeId, DataConstants.ENTITY_CREATED); + return entityActionMsg(customer, customer.getId(), ruleNodeId, ENTITY_CREATED); } + @Override public TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId) { DeviceProfile deviceProfile = null; if (device.getDeviceProfileId() != null) { deviceProfile = mainCtx.getDeviceProfileCache().find(device.getDeviceProfileId()); } - return entityActionMsg(device, device.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, deviceProfile); + return entityActionMsg(device, device.getId(), ruleNodeId, ENTITY_CREATED, deviceProfile); } + @Override public TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId) { AssetProfile assetProfile = null; if (asset.getAssetProfileId() != null) { assetProfile = mainCtx.getAssetProfileCache().find(asset.getAssetProfileId()); } - return entityActionMsg(asset, asset.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, assetProfile); + return entityActionMsg(asset, asset.getId(), ruleNodeId, ENTITY_CREATED, assetProfile); } + @Override public TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action) { + EntityId originator = alarm.getOriginator(); + HasRuleEngineProfile profile = getRuleEngineProfile(originator); + return entityActionMsg(alarm, originator, ruleNodeId, action, profile); + } + + @Override + public TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, TbMsgType actionMsgType) { + EntityId originator = alarm.getOriginator(); + HasRuleEngineProfile profile = getRuleEngineProfile(originator); + return entityActionMsg(alarm, originator, ruleNodeId, actionMsgType, profile); + } + + private HasRuleEngineProfile getRuleEngineProfile(EntityId originator) { HasRuleEngineProfile profile = null; - if (EntityType.DEVICE.equals(alarm.getOriginator().getEntityType())) { - DeviceId deviceId = new DeviceId(alarm.getOriginator().getId()); + if (EntityType.DEVICE.equals(originator.getEntityType())) { + DeviceId deviceId = new DeviceId(originator.getId()); profile = mainCtx.getDeviceProfileCache().get(getTenantId(), deviceId); - } else if (EntityType.ASSET.equals(alarm.getOriginator().getEntityType())) { - AssetId assetId = new AssetId(alarm.getOriginator().getId()); + } else if (EntityType.ASSET.equals(originator.getEntityType())) { + AssetId assetId = new AssetId(originator.getId()); profile = mainCtx.getAssetProfileCache().get(getTenantId(), assetId); } - return entityActionMsg(alarm, alarm.getOriginator(), ruleNodeId, action, profile); + return profile; } + @Override public TbMsg attributesUpdatedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List attributes) { ObjectNode entityNode = JacksonUtil.newObjectNode(); if (attributes != null) { attributes.forEach(attributeKvEntry -> JacksonUtil.addKvEntry(entityNode, attributeKvEntry)); } - return attributesActionMsg(originator, ruleNodeId, scope, DataConstants.ATTRIBUTES_UPDATED, JacksonUtil.toString(entityNode)); + return attributesActionMsg(originator, ruleNodeId, scope, ATTRIBUTES_UPDATED, JacksonUtil.toString(entityNode)); } + @Override public TbMsg attributesDeletedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List keys) { ObjectNode entityNode = JacksonUtil.newObjectNode(); ArrayNode attrsArrayNode = entityNode.putArray("attributes"); if (keys != null) { keys.forEach(attrsArrayNode::add); } - return attributesActionMsg(originator, ruleNodeId, scope, DataConstants.ATTRIBUTES_DELETED, JacksonUtil.toString(entityNode)); + return attributesActionMsg(originator, ruleNodeId, scope, ATTRIBUTES_DELETED, JacksonUtil.toString(entityNode)); } - private TbMsg attributesActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, String action, String msgData) { + private TbMsg attributesActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, TbMsgType actionMsgType, String msgData) { TbMsgMetaData tbMsgMetaData = getActionMetaData(ruleNodeId); tbMsgMetaData.putValue("scope", scope); - HasRuleEngineProfile profile = null; - if (EntityType.DEVICE.equals(originator.getEntityType())) { - DeviceId deviceId = new DeviceId(originator.getId()); - profile = mainCtx.getDeviceProfileCache().get(getTenantId(), deviceId); - } else if (EntityType.ASSET.equals(originator.getEntityType())) { - AssetId assetId = new AssetId(originator.getId()); - profile = mainCtx.getAssetProfileCache().get(getTenantId(), assetId); - } - return entityActionMsg(originator, tbMsgMetaData, msgData, action, profile); + HasRuleEngineProfile profile = getRuleEngineProfile(originator); + return entityActionMsg(originator, tbMsgMetaData, msgData, actionMsgType, profile); } @Override @@ -411,10 +451,11 @@ class DefaultTbContext implements TbContext { mainCtx.getClusterService().onEdgeEventUpdate(tenantId, edgeId); } - public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action) { - return entityActionMsg(entity, id, ruleNodeId, action, null); + public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, TbMsgType actionMsgType) { + return entityActionMsg(entity, id, ruleNodeId, actionMsgType, null); } + @Deprecated(since = "3.6.0", forRemoval = true) public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action, K profile) { try { return entityActionMsg(id, getActionMetaData(ruleNodeId), JacksonUtil.toString(JacksonUtil.valueToTree(entity)), action, profile); @@ -423,6 +464,7 @@ class DefaultTbContext implements TbContext { } } + @Deprecated(since = "3.6.0", forRemoval = true) private TbMsg entityActionMsg(I id, TbMsgMetaData msgMetaData, String msgData, String action, K profile) { String defaultQueueName = null; RuleChainId defaultRuleChainId = null; @@ -433,6 +475,24 @@ class DefaultTbContext implements TbContext { return TbMsg.newMsg(defaultQueueName, action, id, msgMetaData, msgData, defaultRuleChainId, null); } + public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, TbMsgType actionMsgType, K profile) { + try { + return entityActionMsg(id, getActionMetaData(ruleNodeId), JacksonUtil.toString(JacksonUtil.valueToTree(entity)), actionMsgType, profile); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Failed to process " + id.getEntityType().name().toLowerCase() + " " + actionMsgType.name() + " msg: " + e); + } + } + + private TbMsg entityActionMsg(I id, TbMsgMetaData msgMetaData, String msgData, TbMsgType actionMsgType, K profile) { + String defaultQueueName = null; + RuleChainId defaultRuleChainId = null; + if (profile != null) { + defaultQueueName = profile.getDefaultQueueName(); + defaultRuleChainId = profile.getDefaultRuleChainId(); + } + return TbMsg.newMsg(defaultQueueName, actionMsgType, id, msgMetaData, msgData, defaultRuleChainId, null); + } + @Override public RuleNodeId getSelfId() { return nodeCtx.getSelf().getId(); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index 4bd082ee38..cbfd78180b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -16,7 +16,7 @@ package org.thingsboard.server.actors.ruleChain; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorRef; @@ -167,7 +167,8 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor DefaultActorService.RULE_DISPATCHER_NAME, - () -> new RuleNodeActor.ActorCreator(systemContext, tenantId, entityId, ruleChainName, ruleNode.getId())); + () -> new RuleNodeActor.ActorCreator(systemContext, tenantId, entityId, ruleChainName, ruleNode.getId()), + () -> true); } private void initRoutes(RuleChain ruleChain, List ruleNodeList) { @@ -232,7 +233,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor true); } protected TbActorRef getEntityActorRef(EntityId entityId) { diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java index 42a78b7965..dc7529200e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java @@ -22,9 +22,9 @@ import org.thingsboard.server.actors.shared.ComponentMsgProcessor; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventTrigger; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbActorStopReason; -import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventTrigger; public abstract class RuleEngineComponentActor> extends ComponentActor { diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index 11a5895fef..c9f5448df3 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -32,7 +32,6 @@ import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; @@ -82,21 +81,21 @@ public class TenantActor extends RuleChainManagerActor { cantFindTenant = true; log.info("[{}] Started tenant actor for missing tenant.", tenantId); } else { - TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenant.getTenantProfileId()); - isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE); isRuleEngine = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE); if (isRuleEngine) { - try { - if (getApiUsageState().isReExecEnabled()) { - log.debug("[{}] Going to init rule chains", tenantId); - initRuleChains(); - } else { - log.info("[{}] Skip init of the rule chains due to API limits", tenantId); + if (systemContext.getPartitionService().isManagedByCurrentService(tenantId)) { + try { + if (getApiUsageState().isReExecEnabled()) { + log.debug("[{}] Going to init rule chains", tenantId); + initRuleChains(); + } else { + log.info("[{}] Skip init of the rule chains due to API limits", tenantId); + } + } catch (Exception e) { + log.info("Failed to check ApiUsage \"ReExecEnabled\"!!!", e); + cantFindTenant = true; } - } catch (Exception e) { - log.info("Failed to check ApiUsage \"ReExecEnabled\"!!!", e); - cantFindTenant = true; } } log.debug("[{}] Tenant actor started.", tenantId); @@ -270,7 +269,8 @@ public class TenantActor extends RuleChainManagerActor { private TbActorRef getOrCreateDeviceActor(DeviceId deviceId) { return ctx.getOrCreateChildActor(new TbEntityActorId(deviceId), () -> DefaultActorService.DEVICE_DISPATCHER_NAME, - () -> new DeviceActorCreator(systemContext, tenantId, deviceId)); + () -> new DeviceActorCreator(systemContext, tenantId, deviceId), + () -> true); } private void onToEdgeSessionMsg(EdgeSessionMsg msg) { diff --git a/application/src/main/java/org/thingsboard/server/config/CryptoConfig.java b/application/src/main/java/org/thingsboard/server/config/CryptoConfig.java new file mode 100644 index 0000000000..e960d62f8e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/CryptoConfig.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 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.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@Configuration +public class CryptoConfig { + + @Bean + protected BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java index 2ecc8590b8..66c1c9081d 100644 --- a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java @@ -24,10 +24,10 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.exception.TenantProfileNotFoundException; -import org.thingsboard.server.common.msg.tools.TbRateLimitsException; -import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.common.data.limit.LimitedApi; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.service.security.model.SecurityUser; import javax.servlet.FilterChain; diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 0cf820c6f3..55370b04b8 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -81,6 +81,7 @@ public class ThingsboardSecurityConfiguration { public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**"; public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; + public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download"; @Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler; @@ -150,7 +151,8 @@ public class ThingsboardSecurityConfiguration { protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { List pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, - PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT)); + PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT, + DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT)); SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); JwtTokenAuthenticationProcessingFilter filter = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher); @@ -186,11 +188,6 @@ public class ThingsboardSecurityConfiguration { return auth.build(); } - @Bean - protected BCryptPasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - @Autowired private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver; @@ -228,6 +225,7 @@ public class ThingsboardSecurityConfiguration { .antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point .antMatchers(MAIL_OAUTH2_PROCESSING_ENTRY_POINT).permitAll() // Mail oauth2 code processing url + .antMatchers(DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT).permitAll() // Mail oauth2 code processing url .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points .and() .authorizeRequests() diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index 44263d19bd..4e6e1d5824 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -29,6 +29,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -44,6 +45,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; @@ -498,4 +500,21 @@ public class AlarmController extends BaseController { alarmStatus, assigneeId); } + @ApiOperation(value = "Get Alarm Types (getAlarmTypes)", + notes = "Returns a set of unique alarm types based on alarms that are either owned by the tenant or assigned to the customer which user is performing the request.", produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/types", method = RequestMethod.GET) + @ResponseBody + public PageData getAlarmTypes(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException, ExecutionException, InterruptedException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, "type", sortOrder); + return checkNotNull(alarmService.findAlarmTypesByTenantId(getTenantId(), pageLink)); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 4512334d61..ac024093c6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -38,19 +38,18 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.ActivateUserRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest; @@ -123,8 +122,6 @@ public class AuthController extends BaseController { userCredentials.setPassword(passwordEncoder.encode(newPassword)); userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials); - sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED); - eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); ObjectNode response = JacksonUtil.newObjectNode(); response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken()); @@ -259,8 +256,6 @@ public class AuthController extends BaseController { } } - sendEntityNotificationMsg(user.getTenantId(), user.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); - return tokenFactory.createTokenPair(securityUser); } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 68a987a0bc..0af55d7c00 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -60,8 +60,6 @@ import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -142,8 +140,6 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.component.ComponentDiscoveryService; -import org.thingsboard.server.service.edge.instructions.EdgeInstallService; -import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.entitiy.TbNotificationEntityService; import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.ota.OtaPackageStateService; @@ -301,12 +297,6 @@ public abstract class BaseController { @Autowired(required = false) protected EdgeService edgeService; - @Autowired(required = false) - protected EdgeRpcService edgeRpcService; - - @Autowired(required = false) - protected EdgeInstallService edgeInstallService; - @Autowired protected TbNotificationEntityService notificationEntityService; @@ -824,18 +814,6 @@ public abstract class BaseController { } } - protected void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) { - sendNotificationMsgToEdge(tenantId, null, entityId, null, null, action); - } - - protected void sendEntityAssignToEdgeNotificationMsg(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType action) { - sendNotificationMsgToEdge(tenantId, edgeId, entityId, null, null, action); - } - - private void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action) { - tbClusterService.sendNotificationMsgToEdge(tenantId, edgeId, entityId, body, type, action); - } - protected void processDashboardIdFromAdditionalInfo(ObjectNode additionalInfo, String requiredFields) throws ThingsboardException { String dashboardId = additionalInfo.has(requiredFields) ? additionalInfo.get(requiredFields).asText() : null; if (dashboardId != null && !dashboardId.equals("null")) { 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 a6a49f6b3c..e6b4235cf9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -24,6 +24,7 @@ public class ControllerConstants { protected static final String CUSTOMER_ID = "customerId"; protected static final String TENANT_ID = "tenantId"; protected static final String DEVICE_ID = "deviceId"; + protected static final String PROTOCOL = "protocol"; protected static final String EDGE_ID = "edgeId"; protected static final String RPC_ID = "rpcId"; protected static final String ENTITY_ID = "entityId"; @@ -34,6 +35,7 @@ public class ControllerConstants { protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the dashboard id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String PROTOCOL_PARAM_DESCRIPTION = "A string value representing the device connectivity protocol. Possible values: 'mqtt', 'mqtts', 'http', 'https', 'coap', 'coaps'"; protected static final String ENTITY_VIEW_ID_PARAM_DESCRIPTION = "A string value representing the entity view id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String DEVICE_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the device profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; @@ -74,6 +76,7 @@ public class ControllerConstants { protected static final String ASSET_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset name."; protected static final String DASHBOARD_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the dashboard title."; protected static final String WIDGET_BUNDLE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the widget bundle title."; + protected static final String WIDGET_TYPE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the widget type name."; protected static final String RPC_TEXT_SEARCH_DESCRIPTION = "Not implemented. Leave empty."; protected static final String DEVICE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device name."; protected static final String ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the entity view name."; @@ -110,6 +113,7 @@ public class ControllerConstants { protected static final String EDGE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle"; protected static final String RULE_CHAIN_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, root"; protected static final String WIDGET_BUNDLE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, tenantId"; + protected static final String WIDGET_TYPE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, deprecated, tenantId"; protected static final String AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, entityType, entityName, userName, actionType, actionStatus"; protected static final String SORT_ORDER_DESCRIPTION = "Sort order. ASC (ASCENDING) or DESC (DESCENDING)"; protected static final String SORT_ORDER_ALLOWABLE_VALUES = "ASC, DESC"; diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java new file mode 100644 index 0000000000..04b1b4c522 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java @@ -0,0 +1,107 @@ +/** + * Copyright © 2016-2023 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.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.dao.device.DeviceConnectivityService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.system.SystemSecurityService; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.net.URISyntaxException; + +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PROTOCOL; +import static org.thingsboard.server.controller.ControllerConstants.PROTOCOL_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FILE_NAME; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@RequiredArgsConstructor +@Slf4j +public class DeviceConnectivityController extends BaseController { + + private final DeviceConnectivityService deviceConnectivityService; + private final SystemSecurityService systemSecurityService; + + @ApiOperation(value = "Get commands to publish device telemetry (getDevicePublishTelemetryCommands)", + notes = "Fetch the list of commands to publish device telemetry based on device profile " + + "If the user has the authority of 'Tenant Administrator', the server checks that the device is owned by the same tenant. " + + "If the user has the authority of 'Customer User', the server checks that the device is assigned to the same customer. " + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "OK", + examples = @io.swagger.annotations.Example( + value = { + @io.swagger.annotations.ExampleProperty( + mediaType = "application/json", + value = "{\"http\":\"curl -v -X POST http://localhost:8080/api/v1/0ySs4FTOn5WU15XLmal8/telemetry --header Content-Type:application/json --data {temperature:25}\"," + + "\"mqtt\":\"mosquitto_pub -d -q 1 -h localhost -t v1/devices/me/telemetry -i myClient1 -u myUsername1 -P myPassword -m {temperature:25}\"," + + "\"coap\":\"coap-client -m POST coap://localhost:5683/api/v1/0ySs4FTOn5WU15XLmal8/telemetry -t json -e {temperature:25}\"}")}))}) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/device-connectivity/{deviceId}", method = RequestMethod.GET) + @ResponseBody + public JsonNode getDevicePublishTelemetryCommands(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_ID) String strDeviceId, HttpServletRequest request) throws ThingsboardException, URISyntaxException { + checkParameter(DEVICE_ID, strDeviceId); + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + Device device = checkDeviceId(deviceId, Operation.READ_CREDENTIALS); + + String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request); + return deviceConnectivityService.findDevicePublishTelemetryCommands(baseUrl, device); + } + + @ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)", notes = "Download server certificate.") + @RequestMapping(value = "/device-connectivity/{protocol}/certificate/download", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity downloadServerCertificate(@ApiParam(value = PROTOCOL_PARAM_DESCRIPTION) + @PathVariable(PROTOCOL) String protocol) throws ThingsboardException, IOException { + checkParameter(PROTOCOL, protocol); + var pemCert = + checkNotNull(deviceConnectivityService.getPemCertFile(protocol), protocol + " pem cert file is not found!"); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + PEM_CERT_FILE_NAME) + .header("x-filename", PEM_CERT_FILE_NAME) + .contentLength(pemCert.contentLength()) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(pemCert); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index 9a021643cc..a50f9c7658 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -59,6 +59,8 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeBulkImportService; +import org.thingsboard.server.service.edge.instructions.EdgeInstallService; +import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.entitiy.edge.TbEdgeService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; @@ -67,6 +69,7 @@ import org.thingsboard.server.service.security.permission.Resource; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -94,8 +97,11 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @RequestMapping("/api") @RequiredArgsConstructor public class EdgeController extends BaseController { + private final EdgeBulkImportService edgeBulkImportService; private final TbEdgeService tbEdgeService; + private final Optional edgeRpcServiceOpt; + private final Optional edgeInstallServiceOpt; public static final String EDGE_ID = "edgeId"; public static final String EDGE_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the edge is owned by the same tenant. " + @@ -497,13 +503,13 @@ public class EdgeController extends BaseController { @PathVariable("edgeId") String strEdgeId) throws ThingsboardException { checkParameter("edgeId", strEdgeId); final DeferredResult response = new DeferredResult<>(); - if (isEdgesEnabled()) { + if (isEdgesEnabled() && edgeRpcServiceOpt.isPresent()) { EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); edgeId = checkNotNull(edgeId); SecurityUser user = getCurrentUser(); TenantId tenantId = user.getTenantId(); ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId); - edgeRpcService.processSyncRequest(request, fromEdgeSyncResponse -> reply(response, fromEdgeSyncResponse)); + edgeRpcServiceOpt.get().processSyncRequest(request, fromEdgeSyncResponse -> reply(response, fromEdgeSyncResponse)); } else { throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); } @@ -557,9 +563,13 @@ public class EdgeController extends BaseController { @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("edgeId") String strEdgeId, HttpServletRequest request) throws ThingsboardException { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - edgeId = checkNotNull(edgeId); - Edge edge = checkEdgeId(edgeId, Operation.READ); - return checkNotNull(edgeInstallService.getDockerInstallInstructions(getTenantId(), edge, request)); + if (isEdgesEnabled() && edgeInstallServiceOpt.isPresent()) { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + edgeId = checkNotNull(edgeId); + Edge edge = checkEdgeId(edgeId, Operation.READ); + return checkNotNull(edgeInstallServiceOpt.get().getDockerInstallInstructions(getTenantId(), edge, request)); + } else { + throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); + } } } diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java index 08448f1d28..e1e89734b9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java @@ -117,8 +117,8 @@ public class EntityRelationController extends BaseController { tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); } - @ApiOperation(value = "Delete Relations (deleteRelations)", - notes = "Deletes all the relation (both 'from' and 'to' direction) for the specified entity. " + + @ApiOperation(value = "Delete common relations (deleteCommonRelations)", + notes = "Deletes all the relations ('from' and 'to' direction) for the specified entity and relation type group: 'COMMON'. " + SECURITY_CHECKS_ENTITY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"entityId", "entityType"}) @@ -129,7 +129,7 @@ public class EntityRelationController extends BaseController { checkParameter("entityType", strType); EntityId entityId = EntityIdFactory.getByTypeAndId(strType, strId); checkEntityId(entityId, Operation.WRITE); - tbEntityRelationService.deleteRelations(getTenantId(), getCurrentUser().getCustomerId(), entityId, getCurrentUser()); + tbEntityRelationService.deleteCommonRelations(getTenantId(), getCurrentUser().getCustomerId(), entityId, getCurrentUser()); } @ApiOperation(value = "Get Relation (getRelation)", diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index 0f4a288e33..a88a7ee4f6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -44,6 +44,8 @@ import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequestInfo; import org.thingsboard.server.common.data.notification.NotificationRequestPreview; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; +import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; @@ -294,15 +296,27 @@ public class NotificationController extends BaseController { int recipientsCount; List recipientsPart; NotificationTargetType targetType = target.getConfiguration().getType(); - if (targetType == NotificationTargetType.PLATFORM_USERS) { - PageData recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), - (PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null, - new SortOrder("createdTime", SortOrder.Direction.DESC))); - recipientsCount = (int) recipients.getTotalElements(); - recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList()); - } else { - recipientsCount = 1; - recipientsPart = List.of(((SlackNotificationTargetConfig) target.getConfiguration()).getConversation()); + switch (targetType) { + case PLATFORM_USERS: { + PageData recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), + (PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null, + SortOrder.BY_CREATED_TIME_DESC)); + recipientsCount = (int) recipients.getTotalElements(); + recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList()); + break; + } + case SLACK: { + recipientsCount = 1; + recipientsPart = List.of(((SlackNotificationTargetConfig) target.getConfiguration()).getConversation()); + break; + } + case MICROSOFT_TEAMS: { + recipientsCount = 1; + recipientsPart = List.of(((MicrosoftTeamsNotificationTargetConfig) target.getConfiguration())); + break; + } + default: + throw new IllegalArgumentException("Target type " + targetType + " not supported"); } firstRecipient.putIfAbsent(targetType, !recipientsPart.isEmpty() ? recipientsPart.get(0) : null); for (NotificationRecipient recipient : recipientsPart) { @@ -431,10 +445,23 @@ public class NotificationController extends BaseController { notes = "Returns the list of delivery methods that are properly configured and are allowed to be used for sending notifications." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @GetMapping("/notification/deliveryMethods") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public Set getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { - accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ); return notificationCenter.getAvailableDeliveryMethods(user.getTenantId()); } + + @PostMapping("/notification/settings/user") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public UserNotificationSettings saveUserNotificationSettings(@RequestBody @Valid UserNotificationSettings settings, + @AuthenticationPrincipal SecurityUser user) { + return notificationSettingsService.saveUserNotificationSettings(user.getTenantId(), user.getId(), settings); + } + + @GetMapping("/notification/settings/user") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public UserNotificationSettings getUserNotificationSettings(@AuthenticationPrincipal SecurityUser user) { + return notificationSettingsService.getUserNotificationSettings(user.getTenantId(), user.getId(), true); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueController.java b/application/src/main/java/org/thingsboard/server/controller/QueueController.java index 0e79ae7955..faeea1a4aa 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -126,7 +126,6 @@ public class QueueController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @RequestMapping(value = "/queues", params = {"serviceType"}, method = RequestMethod.POST) @ResponseBody - public Queue saveQueue(@ApiParam(value = "A JSON value representing the queue.") @RequestBody Queue queue, @ApiParam(value = QUEUE_SERVICE_TYPE_DESCRIPTION, allowableValues = QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES, required = true) diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java index 827788b34b..d8876eaa9c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rpc.Rpc; @@ -52,7 +53,6 @@ import org.thingsboard.server.service.security.permission.Operation; import javax.annotation.Nullable; import java.util.UUID; -import static org.thingsboard.server.common.data.DataConstants.RPC_DELETED; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; @@ -239,7 +239,7 @@ public class RpcV2Controller extends AbstractRpcController { rpcService.deleteRpc(getTenantId(), rpcId); rpc.setStatus(RpcStatus.DELETED); - TbMsg msg = TbMsg.newMsg(RPC_DELETED, rpc.getDeviceId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(rpc)); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_DELETED, rpc.getDeviceId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(rpc)); tbClusterService.pushMsgToRuleEngine(getTenantId(), rpc.getDeviceId(), msg, null); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index 05f56e93bd..2445ee9bb6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -201,7 +201,7 @@ public class TelemetryController extends BaseController { @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope) throws ThingsboardException { return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeKeysCallback(result, tenantId, entityId, scope)); + (result, tenantId, entityId) -> getAttributeKeysCallback(result, tenantId, entityId, scope)); } @ApiOperation(value = "Get attributes (getAttributes)", @@ -219,9 +219,9 @@ public class TelemetryController extends BaseController { @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType, @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { - SecurityUser user = getCurrentUser(); + SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr)); + (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr)); } @@ -245,7 +245,7 @@ public class TelemetryController extends BaseController { @ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr)); + (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr)); } @ApiOperation(value = "Get time-series keys (getTimeseriesKeys)", @@ -259,7 +259,7 @@ public class TelemetryController extends BaseController { @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType, @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException { return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, - (result, tenantId, entityId) -> Futures.addCallback(tsService.findAllLatest(tenantId, entityId), getTsKeysToResponseCallback(result), MoreExecutors.directExecutor())); + (result, tenantId, entityId) -> Futures.addCallback(tsService.findAllLatest(tenantId, entityId), getTsKeysToResponseCallback(result), MoreExecutors.directExecutor())); } @ApiOperation(value = "Get latest time-series value (getLatestTimeseries)", @@ -462,7 +462,9 @@ public class TelemetryController extends BaseController { notes = "Delete time-series for selected entity based on entity id, entity type and keys." + " Use 'deleteAllDataForKeys' to delete all time-series data." + " Use 'startTs' and 'endTs' to specify time-range instead. " + - " Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) after deletion of the time range. " + + " Use 'deleteLatest' to delete latest value (stored in separate table for performance) if the value's timestamp matches the time-range. " + + " Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) if the value's timestamp matches the time-range and 'deleteLatest' param is true." + + " The replacement value will be fetched from the 'time-series' table, and its timestamp will be the most recent one before the defined time-range. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @ApiResponses(value = { @@ -486,14 +488,16 @@ public class TelemetryController extends BaseController { @RequestParam(name = "startTs", required = false) Long startTs, @ApiParam(value = "A long value representing the end timestamp of removal time range in milliseconds.") @RequestParam(name = "endTs", required = false) Long endTs, + @ApiParam(value = "If the parameter is set to true, the latest telemetry can be removed, otherwise, in case that parameter is set to false the latest value will not removed.") + @RequestParam(name = "deleteLatest", required = false, defaultValue = "true") boolean deleteLatest, @ApiParam(value = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.") @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted); + return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest); } private DeferredResult deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys, - Long startTs, Long endTs, boolean rewriteLatestIfDeleted) throws ThingsboardException { + Long startTs, Long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) throws ThingsboardException { List keys = toKeysList(keysStr); if (keys.isEmpty()) { return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST); @@ -517,7 +521,7 @@ public class TelemetryController extends BaseController { return accessValidator.validateEntityAndCallback(user, Operation.WRITE_TELEMETRY, entityIdStr, (result, tenantId, entityId) -> { List deleteTsKvQueries = new ArrayList<>(); for (String key : keys) { - deleteTsKvQueries.add(new BaseDeleteTsKvQuery(key, deleteFromTs, deleteToTs, rewriteLatestIfDeleted)); + deleteTsKvQueries.add(new BaseDeleteTsKvQuery(key, deleteFromTs, deleteToTs, rewriteLatestIfDeleted, deleteLatest)); } tsSubService.deleteTimeseriesAndNotify(tenantId, entityId, keys, deleteTsKvQueries, new FutureCallback<>() { @Override 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 d1ecd0441a..4aeaeb2210 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -17,7 +17,7 @@ package org.thingsboard.server.controller; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; -import lombok.extern.slf4j.Slf4j; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -28,10 +28,13 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; +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.security.Authority; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; @@ -39,27 +42,40 @@ import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.List; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; +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.SORT_ORDER_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.ControllerConstants.WIDGET_TYPE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.WIDGET_TYPE_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.WIDGET_TYPE_TEXT_SEARCH_DESCRIPTION; -@Slf4j @RestController @TbCoreComponent @RequestMapping("/api") +@RequiredArgsConstructor public class WidgetTypeController extends AutoCommitController { + private final TbWidgetTypeService tbWidgetTypeService; + private static final String WIDGET_TYPE_DESCRIPTION = "Widget Type represents the template for widget creation. Widget Type and Widget are similar to class and object in OOP theory."; private static final String WIDGET_TYPE_DETAILS_DESCRIPTION = "Widget Type Details extend Widget Type and add image and description properties. " + "Those properties are useful to edit the Widget Type but they are not required for Dashboard rendering. "; 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 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"; @ApiOperation(value = "Get Widget Type Details (getWidgetTypeById)", notes = "Get the Widget Type Details based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @@ -74,13 +90,26 @@ public class WidgetTypeController extends AutoCommitController { return checkWidgetTypeId(widgetTypeId, Operation.READ); } + @ApiOperation(value = "Get Widget Type Info (getWidgetTypeInfoById)", + notes = "Get the Widget Type Info based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/widgetTypeInfo/{widgetTypeId}", method = RequestMethod.GET) + @ResponseBody + public WidgetTypeInfo getWidgetTypeInfoById( + @ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { + checkParameter("widgetTypeId", strWidgetTypeId); + WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); + return new WidgetTypeInfo(checkWidgetTypeId(widgetTypeId, Operation.READ)); + } + @ApiOperation(value = "Create Or Update Widget Type (saveWidgetType)", notes = "Create or update the Widget Type. " + WIDGET_TYPE_DESCRIPTION + " " + "When creating the Widget Type, platform generates Widget Type Id as " + UUID_WIKI_LINK + "The newly created Widget Type Id will be present in the response. " + "Specify existing Widget Type id to update the Widget Type. " + "Referencing non-existing Widget Type Id will cause 'Not Found' error." + - "\n\nWidget Type alias is unique in the scope of Widget Bundle. " + + "\n\nWidget Type fqn is unique in the scope of System or Tenant. " + "Special Tenant Id '13814000-1dd2-11b2-8080-808080808080' is automatically used if the create request is sent by user with 'SYS_ADMIN' authority." + "Remove 'id', 'tenantId' rom the request body example (below) to create new Widget Type entity." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @@ -89,7 +118,9 @@ public class WidgetTypeController extends AutoCommitController { @ResponseBody public WidgetTypeDetails saveWidgetType( @ApiParam(value = "A JSON value representing the Widget Type Details.", required = true) - @RequestBody WidgetTypeDetails widgetTypeDetails) throws Exception { + @RequestBody WidgetTypeDetails widgetTypeDetails, + @ApiParam(value = UPDATE_EXISTING_BY_FQN_PARAM_DESCRIPTION) + @RequestParam(required = false) Boolean updateExistingByFqn) throws Exception { var currentUser = getCurrentUser(); if (Authority.SYS_ADMIN.equals(currentUser.getAuthority())) { widgetTypeDetails.setTenantId(TenantId.SYS_TENANT_ID); @@ -98,19 +129,7 @@ public class WidgetTypeController extends AutoCommitController { } checkEntity(widgetTypeDetails.getId(), widgetTypeDetails, Resource.WIDGET_TYPE); - WidgetTypeDetails savedWidgetTypeDetails = widgetTypeService.saveWidgetType(widgetTypeDetails); - - if (!Authority.SYS_ADMIN.equals(currentUser.getAuthority())) { - WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(widgetTypeDetails.getTenantId(), widgetTypeDetails.getBundleAlias()); - if (widgetsBundle != null) { - autoCommit(currentUser, widgetsBundle.getId()); - } - } - - sendEntityNotificationMsg(getTenantId(), savedWidgetTypeDetails.getId(), - widgetTypeDetails.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); - - return checkNotNull(savedWidgetTypeDetails); + return tbWidgetTypeService.save(widgetTypeDetails, updateExistingByFqn != null && updateExistingByFqn, currentUser); } @ApiOperation(value = "Delete widget type (deleteWidgetType)", @@ -122,27 +141,50 @@ public class WidgetTypeController extends AutoCommitController { @ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("widgetTypeId") String strWidgetTypeId) throws Exception { checkParameter("widgetTypeId", strWidgetTypeId); - var currentUser = getCurrentUser(); WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); WidgetTypeDetails wtd = checkWidgetTypeId(widgetTypeId, Operation.DELETE); - widgetTypeService.deleteWidgetType(currentUser.getTenantId(), widgetTypeId); + tbWidgetTypeService.delete(wtd, getCurrentUser()); + } - if (wtd != null && !Authority.SYS_ADMIN.equals(currentUser.getAuthority())) { - WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(wtd.getTenantId(), wtd.getBundleAlias()); - if (widgetsBundle != null) { - autoCommit(currentUser, widgetsBundle.getId()); + @ApiOperation(value = "Get Widget Types (getWidgetTypes)", + notes = "Returns a page of Widget Type objects available for current user. " + WIDGET_TYPE_DESCRIPTION + " " + + PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/widgetTypes", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getWidgetTypes( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = WIDGET_TYPE_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = WIDGET_TYPE_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder, + @ApiParam(value = TENANT_ONLY_PARAM_DESCRIPTION) + @RequestParam(required = false) Boolean tenantOnly, + @ApiParam(value = FULL_SEARCH_PARAM_DESCRIPTION) + @RequestParam(required = false) Boolean fullSearch) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { + return checkNotNull(widgetTypeService.findSystemWidgetTypesByPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink)); + } else { + if (tenantOnly != null && tenantOnly) { + return checkNotNull(widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink)); + } else { + return checkNotNull(widgetTypeService.findAllTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink)); } } - - sendEntityNotificationMsg(getTenantId(), widgetTypeId, EdgeEventActionType.DELETED); } - @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", + @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypesByBundleAlias) (Deprecated)", notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) @ResponseBody - public List getBundleWidgetTypes( + public List getBundleWidgetTypesByBundleAlias( @ApiParam(value = "System or Tenant", required = true) @RequestParam boolean isSystem, @ApiParam(value = "Widget Bundle alias", required = true) @@ -153,15 +195,28 @@ public class WidgetTypeController extends AutoCommitController { } else { tenantId = getCurrentUser().getTenantId(); } - return checkNotNull(widgetTypeService.findWidgetTypesByTenantIdAndBundleAlias(tenantId, bundleAlias)); + WidgetsBundle widgetsBundle = checkNotNull(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(tenantId, bundleAlias)); + return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundle.getId())); } - @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypes)", + @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", + notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/widgetTypes", params = {"widgetsBundleId"}, method = RequestMethod.GET) + @ResponseBody + public List getBundleWidgetTypes( + @ApiParam(value = "Widget Bundle Id", required = true) + @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); + return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundleId)); + } + + @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetailsByBundleAlias) (Deprecated)", notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) @ResponseBody - public List getBundleWidgetTypesDetails( + public List getBundleWidgetTypesDetailsByBundleAlias( @ApiParam(value = "System or Tenant", required = true) @RequestParam boolean isSystem, @ApiParam(value = "Widget Bundle alias", required = true) @@ -172,15 +227,40 @@ public class WidgetTypeController extends AutoCommitController { } else { tenantId = getCurrentUser().getTenantId(); } - return checkNotNull(widgetTypeService.findWidgetTypesDetailsByTenantIdAndBundleAlias(tenantId, bundleAlias)); + WidgetsBundle widgetsBundle = checkNotNull(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(tenantId, bundleAlias)); + return checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundle.getId())); } - @ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfos)", - notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypes)", + notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/widgetTypesDetails", params = {"widgetsBundleId"}, method = RequestMethod.GET) + @ResponseBody + public List getBundleWidgetTypesDetails( + @ApiParam(value = "Widget Bundle Id", required = true) + @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); + return checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId)); + } + + @ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)", + notes = "Returns an array of Widget Type fqns that belong to specified Widget Bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/widgetTypeFqns", params = {"widgetsBundleId"}, method = RequestMethod.GET) + @ResponseBody + public List getBundleWidgetTypeFqns( + @ApiParam(value = "Widget Bundle Id", required = true) + @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); + return checkNotNull(widgetTypeService.findWidgetFqnsByWidgetsBundleId(getTenantId(), widgetsBundleId)); + } + + @ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfosByBundleAlias) (Deprecated)", + notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) @ResponseBody - public List getBundleWidgetTypesInfos( + public List getBundleWidgetTypesInfosByBundleAlias( @ApiParam(value = "System or Tenant", required = true) @RequestParam boolean isSystem, @ApiParam(value = "Widget Bundle alias", required = true) @@ -191,15 +271,28 @@ public class WidgetTypeController extends AutoCommitController { } else { tenantId = getCurrentUser().getTenantId(); } - return checkNotNull(widgetTypeService.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId, bundleAlias)); + WidgetsBundle widgetsBundle = checkNotNull(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(tenantId, bundleAlias)); + return checkNotNull(widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(getTenantId(), widgetsBundle.getId())); } - @ApiOperation(value = "Get Widget Type (getWidgetType)", + @ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfos)", + notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/widgetTypesInfos", params = {"widgetsBundleId"}, method = RequestMethod.GET) + @ResponseBody + public List getBundleWidgetTypesInfos( + @ApiParam(value = "Widget Bundle Id", required = true) + @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); + return checkNotNull(widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(getTenantId(), widgetsBundleId)); + } + + @ApiOperation(value = "Get Widget Type (getWidgetTypeByBundleAliasAndTypeAlias) (Deprecated)", notes = "Get the Widget Type based on the provided parameters. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"}, method = RequestMethod.GET) @ResponseBody - public WidgetType getWidgetType( + public WidgetType getWidgetTypeByBundleAliasAndTypeAlias( @ApiParam(value = "System or Tenant", required = true) @RequestParam boolean isSystem, @ApiParam(value = "Widget Bundle alias", required = true) @@ -212,7 +305,33 @@ public class WidgetTypeController extends AutoCommitController { } else { tenantId = getCurrentUser().getTenantId(); } - WidgetType widgetType = widgetTypeService.findWidgetTypeByTenantIdBundleAliasAndAlias(tenantId, bundleAlias, alias); + WidgetType widgetType = widgetTypeService.findWidgetTypeByTenantIdAndFqn(tenantId, bundleAlias + "." + alias); + checkNotNull(widgetType); + accessControlService.checkPermission(getCurrentUser(), Resource.WIDGET_TYPE, Operation.READ, widgetType.getId(), widgetType); + return widgetType; + } + + @ApiOperation(value = "Get Widget Type (getWidgetType)", + notes = "Get the Widget Type by FQN. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/widgetType", params = {"fqn"}, method = RequestMethod.GET) + @ResponseBody + public WidgetType getWidgetType( + @ApiParam(value = "Widget Type fqn", required = true) + @RequestParam String fqn) throws ThingsboardException { + String[] parts = fqn.split("\\."); + String scopeQualifier = parts.length > 0 ? parts[0] : null; + if (parts.length < 2 || (!scopeQualifier.equals("system") && !scopeQualifier.equals("tenant"))) { + throw new ThingsboardException("Invalid fqn!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + TenantId tenantId; + if ("system".equals(scopeQualifier)) { + tenantId = TenantId.fromUUID(ModelConstants.NULL_UUID); + } else { + tenantId = getCurrentUser().getTenantId(); + } + String typeFqn = fqn.substring(scopeQualifier.length() + 1); + WidgetType widgetType = widgetTypeService.findWidgetTypeByTenantIdAndFqn(tenantId, typeFqn); checkNotNull(widgetType); accessControlService.checkPermission(getCurrentUser(), Resource.WIDGET_TYPE, Operation.READ, widgetType.getId(), widgetType); return widgetType; 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 737e6d54d5..382546dc96 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -40,7 +41,10 @@ import org.thingsboard.server.service.entitiy.widgets.bundle.TbWidgetsBundleServ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; @@ -106,6 +110,49 @@ public class WidgetsBundleController extends BaseController { return tbWidgetsBundleService.save(widgetsBundle, currentUser); } + @ApiOperation(value = "Update widgets bundle widgets types list (updateWidgetsBundleWidgetTypes)", + notes = "Updates widgets bundle widgets list." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypes", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public void updateWidgetsBundleWidgetTypes( + @ApiParam(value = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("widgetsBundleId") String strWidgetsBundleId, + @ApiParam(value = "Ordered list of widget type Ids to be included by widgets bundle") + @RequestBody List strWidgetTypeIds) throws Exception { + checkParameter("widgetsBundleId", strWidgetsBundleId); + WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); + checkNotNull(strWidgetTypeIds); + Set widgetTypeIds = new LinkedHashSet<>(); + var currentUser = getCurrentUser(); + TenantId tenantId = currentUser.getTenantId(); + for (String strWidgetTypeId : strWidgetTypeIds) { + WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); + if (!widgetTypeIds.contains(widgetTypeId) && + widgetTypeService.widgetTypeExistsByTenantIdAndWidgetTypeId(tenantId, widgetTypeId)) { + widgetTypeIds.add(widgetTypeId); + } + } + tbWidgetsBundleService.updateWidgetsBundleWidgetTypes(widgetsBundleId, new ArrayList<>(widgetTypeIds), currentUser); + } + + @ApiOperation(value = "Update widgets bundle widgets list from widget type FQNs list (updateWidgetsBundleWidgetFqns)", + notes = "Updates widgets bundle widgets list from widget type FQNs list." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypeFqns", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public void updateWidgetsBundleWidgetFqns( + @ApiParam(value = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("widgetsBundleId") String strWidgetsBundleId, + @ApiParam(value = "Ordered list of widget type FQNs to be included by widgets bundle") + @RequestBody List widgetTypeFqns) throws Exception { + checkParameter("widgetsBundleId", strWidgetsBundleId); + WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); + checkNotNull(widgetTypeFqns); + var currentUser = getCurrentUser(); + tbWidgetsBundleService.updateWidgetsBundleWidgetFqns(widgetsBundleId, widgetTypeFqns, currentUser); + } + @ApiOperation(value = "Delete widgets bundle (deleteWidgetsBundle)", notes = "Deletes the widget bundle. Referencing non-existing Widget Bundle Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 91583136f6..e1f20be0e4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -33,10 +33,10 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.config.WebSocketConfiguration; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; @@ -221,12 +221,12 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke .build(); } - private class SessionMetaData implements SendHandler { + class SessionMetaData implements SendHandler { private final WebSocketSession session; private final RemoteEndpoint.Async asyncRemote; private final WebSocketSessionRef sessionRef; - private final AtomicBoolean isSending = new AtomicBoolean(false); + final AtomicBoolean isSending = new AtomicBoolean(false); private final Queue> msgQueue; private volatile long lastActivityTime; @@ -241,7 +241,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke this.lastActivityTime = System.currentTimeMillis(); } - synchronized void sendPing(long currentTime) { + void sendPing(long currentTime) { try { long timeSinceLastActivity = currentTime - lastActivityTime; if (timeSinceLastActivity >= pingTimeout) { @@ -256,37 +256,38 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke } } - private void closeSession(CloseStatus reason) { + void closeSession(CloseStatus reason) { try { close(this.sessionRef, reason); } catch (IOException ioe) { log.trace("[{}] Session transport error", session.getId(), ioe); + } finally { + msgQueue.clear(); } } - synchronized void processPongMessage(long currentTime) { + void processPongMessage(long currentTime) { lastActivityTime = currentTime; } - synchronized void sendMsg(String msg) { + void sendMsg(String msg) { sendMsg(new TbWebSocketTextMsg(msg)); } - synchronized void sendMsg(TbWebSocketMsg msg) { - if (isSending.compareAndSet(false, true)) { - sendMsgInternal(msg); - } else { - try { - msgQueue.add(msg); - } catch (RuntimeException e) { - if (log.isTraceEnabled()) { - log.trace("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId(), e); - } else { - log.info("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId()); - } - closeSession(CloseStatus.POLICY_VIOLATION.withReason("Max pending updates limit reached!")); + void sendMsg(TbWebSocketMsg msg) { + try { + msgQueue.add(msg); + } catch (RuntimeException e) { + if (log.isTraceEnabled()) { + log.trace("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId(), e); + } else { + log.info("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId()); } + closeSession(CloseStatus.POLICY_VIOLATION.withReason("Max pending updates limit reached!")); + return; } + + processNextMsg(); } private void sendMsgInternal(TbWebSocketMsg msg) { @@ -294,9 +295,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (TbWebSocketMsgType.TEXT.equals(msg.getType())) { TbWebSocketTextMsg textMsg = (TbWebSocketTextMsg) msg; this.asyncRemote.sendText(textMsg.getMsg(), this); + // isSending status will be reset in the onResult method by call back } else { TbWebSocketPingMsg pingMsg = (TbWebSocketPingMsg) msg; - this.asyncRemote.sendPing(pingMsg.getMsg()); + this.asyncRemote.sendPing(pingMsg.getMsg()); // blocking call + isSending.set(false); processNextMsg(); } } catch (Exception e) { @@ -310,12 +313,17 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (!result.isOK()) { log.trace("[{}] Failed to send msg", session.getId(), result.getException()); closeSession(CloseStatus.SESSION_NOT_RELIABLE); - } else { - processNextMsg(); + return; } + + isSending.set(false); + processNextMsg(); } private void processNextMsg() { + if (msgQueue.isEmpty() || !isSending.compareAndSet(false, true)) { + return; + } TbWebSocketMsg msg = msgQueue.poll(); if (msg != null) { sendMsgInternal(msg); @@ -392,19 +400,21 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (tenantProfileConfiguration == null) { return true; } - + boolean limitAllowed; String sessionId = session.getId(); if (tenantProfileConfiguration.getMaxWsSessionsPerTenant() > 0) { Set tenantSessions = tenantSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet()); synchronized (tenantSessions) { - if (tenantSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerTenant()) { + limitAllowed = tenantSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerTenant(); + if (limitAllowed) { tenantSessions.add(sessionId); - } else { + } + } + if (!limitAllowed) { log.info("[{}][{}][{}] Failed to start session. Max tenant sessions limit reached" , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); session.close(CloseStatus.POLICY_VIOLATION.withReason("Max tenant sessions limit reached!")); return false; - } } } @@ -412,42 +422,48 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (tenantProfileConfiguration.getMaxWsSessionsPerCustomer() > 0) { Set customerSessions = customerSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet()); synchronized (customerSessions) { - if (customerSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerCustomer()) { + limitAllowed = customerSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerCustomer(); + if (limitAllowed) { customerSessions.add(sessionId); - } else { + } + } + if (!limitAllowed) { log.info("[{}][{}][{}] Failed to start session. Max customer sessions limit reached" , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); session.close(CloseStatus.POLICY_VIOLATION.withReason("Max customer sessions limit reached")); return false; - } } } if (tenantProfileConfiguration.getMaxWsSessionsPerRegularUser() > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { Set regularUserSessions = regularUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); synchronized (regularUserSessions) { - if (regularUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerRegularUser()) { + limitAllowed = regularUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerRegularUser(); + if (limitAllowed) { regularUserSessions.add(sessionId); - } else { + } + } + if (!limitAllowed) { log.info("[{}][{}][{}] Failed to start session. Max regular user sessions limit reached" , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); session.close(CloseStatus.POLICY_VIOLATION.withReason("Max regular user sessions limit reached")); return false; - } } } if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { Set publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); synchronized (publicUserSessions) { - if (publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerPublicUser()) { + limitAllowed = publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerPublicUser(); + if (limitAllowed) { publicUserSessions.add(sessionId); - } else { + } + } + if (!limitAllowed) { log.info("[{}][{}][{}] Failed to start session. Max public user sessions limit reached" , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); session.close(CloseStatus.POLICY_VIOLATION.withReason("Max public user sessions limit reached")); return false; - } } } } diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index ed641c2d4f..0cd581b2e5 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -16,7 +16,6 @@ package org.thingsboard.server.exception; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 7f4133e223..208da7d36c 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -258,7 +258,7 @@ public class ThingsboardInstallService { log.info("Upgrading ThingsBoard from version 3.5.0 to 3.5.1 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.5.0"); case "3.5.1": - log.info("Upgrading ThingsBoard from version 3.5.1 to 3.5.2 ..."); + log.info("Upgrading ThingsBoard from version 3.5.1 to 3.6.0 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.5.1"); dataUpdateService.updateData("3.5.1"); systemDataLoaderService.updateDefaultNotificationConfigs(); diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index e068cd4d23..99884b013d 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -26,7 +26,6 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; @@ -38,19 +37,21 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentTrigger; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger; +import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitTrigger; +import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionTrigger; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; -import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentTrigger; -import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger; -import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitTrigger; -import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionTrigger; import org.thingsboard.server.dao.audit.AuditLogService; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; @Service @@ -63,85 +64,8 @@ public class EntityActionService { public void pushEntityActionToRuleEngine(EntityId entityId, HasName entity, TenantId tenantId, CustomerId customerId, ActionType actionType, User user, Object... additionalInfo) { - String msgType = null; - switch (actionType) { - case ADDED: - msgType = DataConstants.ENTITY_CREATED; - break; - case DELETED: - msgType = DataConstants.ENTITY_DELETED; - break; - case UPDATED: - msgType = DataConstants.ENTITY_UPDATED; - break; - case ASSIGNED_TO_CUSTOMER: - msgType = DataConstants.ENTITY_ASSIGNED; - break; - case UNASSIGNED_FROM_CUSTOMER: - msgType = DataConstants.ENTITY_UNASSIGNED; - break; - case ATTRIBUTES_UPDATED: - msgType = DataConstants.ATTRIBUTES_UPDATED; - break; - case ATTRIBUTES_DELETED: - msgType = DataConstants.ATTRIBUTES_DELETED; - break; - case ALARM_ACK: - msgType = DataConstants.ALARM_ACK; - break; - case ALARM_CLEAR: - msgType = DataConstants.ALARM_CLEAR; - break; - case ALARM_ASSIGNED: - msgType = DataConstants.ALARM_ASSIGNED; - break; - case ALARM_UNASSIGNED: - msgType = DataConstants.ALARM_UNASSIGNED; - break; - case ALARM_DELETE: - msgType = DataConstants.ALARM_DELETE; - break; - case ADDED_COMMENT: - msgType = DataConstants.COMMENT_CREATED; - break; - case UPDATED_COMMENT: - msgType = DataConstants.COMMENT_UPDATED; - break; - case ASSIGNED_FROM_TENANT: - msgType = DataConstants.ENTITY_ASSIGNED_FROM_TENANT; - break; - case ASSIGNED_TO_TENANT: - msgType = DataConstants.ENTITY_ASSIGNED_TO_TENANT; - break; - case PROVISION_SUCCESS: - msgType = DataConstants.PROVISION_SUCCESS; - break; - case PROVISION_FAILURE: - msgType = DataConstants.PROVISION_FAILURE; - break; - case TIMESERIES_UPDATED: - msgType = DataConstants.TIMESERIES_UPDATED; - break; - case TIMESERIES_DELETED: - msgType = DataConstants.TIMESERIES_DELETED; - break; - case ASSIGNED_TO_EDGE: - msgType = DataConstants.ENTITY_ASSIGNED_TO_EDGE; - break; - case UNASSIGNED_FROM_EDGE: - msgType = DataConstants.ENTITY_UNASSIGNED_FROM_EDGE; - break; - case RELATION_ADD_OR_UPDATE: - msgType = DataConstants.RELATION_ADD_OR_UPDATE; - break; - case RELATION_DELETED: - msgType = DataConstants.RELATION_DELETED; - break; - case RELATIONS_DELETED: - msgType = DataConstants.RELATIONS_DELETED; - break; - } - if (!StringUtils.isEmpty(msgType)) { + Optional msgType = actionType.getRuleEngineMsgType(); + if (msgType.isPresent()) { try { TbMsgMetaData metaData = new TbMsgMetaData(); if (user != null) { @@ -247,7 +171,7 @@ public class EntityActionService { if (tenantId != null && !tenantId.isSysTenantId()) { processNotificationRules(tenantId, entityId, entity, actionType, user, additionalInfo); } - TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); + TbMsg tbMsg = TbMsg.newMsg(msgType.get(), entityId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null); } catch (Exception e) { log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e); @@ -313,10 +237,6 @@ public class EntityActionService { auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo); } - public void sendEntityNotificationMsgToEdge(TenantId tenantId, EntityId entityId, EdgeEventActionType action) { - tbClusterService.sendNotificationMsgToEdge(tenantId, null, entityId, null, null, action); - } - private T extractParameter(Class clazz, int index, Object... additionalInfo) { T result = null; if (additionalInfo != null && additionalInfo.length > index) { diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java index ad65ee805b..112c406d19 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java @@ -30,8 +30,12 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeDefinition; import org.thingsboard.rule.engine.api.RuleNode; -import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.TbVersionedNode; +import org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode; +import org.thingsboard.rule.engine.filter.TbOriginatorTypeSwitchNode; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -194,7 +198,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe scannedComponent.setName(ruleNodeAnnotation.name()); scannedComponent.setScope(ruleNodeAnnotation.scope()); scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode()); - NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation); + NodeDefinition nodeDefinition = prepareNodeDefinition(clazz, ruleNodeAnnotation); ObjectNode configurationDescriptor = JacksonUtil.newObjectNode(); JsonNode node = JacksonUtil.valueToTree(nodeDefinition); configurationDescriptor.set("nodeDefinition", node); @@ -221,13 +225,13 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe return scannedComponent; } - private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws Exception { + private NodeDefinition prepareNodeDefinition(Class clazz, RuleNode nodeAnnotation) throws Exception { NodeDefinition nodeDefinition = new NodeDefinition(); nodeDefinition.setDetails(nodeAnnotation.nodeDetails()); nodeDefinition.setDescription(nodeAnnotation.nodeDescription()); nodeDefinition.setInEnabled(nodeAnnotation.inEnabled()); nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled()); - nodeDefinition.setRelationTypes(getRelationTypesWithFailureRelation(nodeAnnotation)); + nodeDefinition.setRelationTypes(getRelationTypesWithFailureRelation(clazz, nodeAnnotation)); nodeDefinition.setCustomRelations(nodeAnnotation.customRelations()); nodeDefinition.setRuleChainNode(nodeAnnotation.ruleChainNode()); Class configClazz = nodeAnnotation.configClazz(); @@ -242,10 +246,17 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe return nodeDefinition; } - private String[] getRelationTypesWithFailureRelation(RuleNode nodeAnnotation) { + private String[] getRelationTypesWithFailureRelation(Class clazz, RuleNode nodeAnnotation) { List relationTypes = new ArrayList<>(Arrays.asList(nodeAnnotation.relationTypes())); - if (!relationTypes.contains(TbRelationTypes.FAILURE)) { - relationTypes.add(TbRelationTypes.FAILURE); + if (TbOriginatorTypeSwitchNode.class.equals(clazz)) { + relationTypes.addAll(EntityType.NORMAL_NAMES); + } + if (TbMsgTypeSwitchNode.class.equals(clazz)) { + relationTypes.addAll(TbMsgType.NODE_CONNECTIONS); + relationTypes.add(TbNodeConnectionType.OTHER); + } + if (!relationTypes.contains(TbNodeConnectionType.FAILURE)) { + relationTypes.add(TbNodeConnectionType.FAILURE); } return relationTypes.toArray(new String[relationTypes.size()]); } diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java index beecebe2ba..763ce01116 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.TbMsg; @@ -162,7 +163,7 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) { if (targetDevice != null) { log.warn("[{}] The device is present and could not be provisioned once more!", targetDevice.getName()); - notify(targetDevice, provisionRequest, DataConstants.PROVISION_FAILURE, false); + notify(targetDevice, provisionRequest, TbMsgType.PROVISION_FAILURE, false); throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); } else { return createDevice(provisionRequest, targetProfile); @@ -190,11 +191,11 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { Optional provisionState = attributesService.find(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE, DEVICE_PROVISION_STATE).get(); if (provisionState != null && provisionState.isPresent() && !provisionState.get().getValueAsString().equals(PROVISIONED_STATE)) { - notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false); + notify(device, provisionRequest, TbMsgType.PROVISION_FAILURE, false); throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); } else { saveProvisionStateAttribute(device).get(); - notify(device, provisionRequest, DataConstants.PROVISION_SUCCESS, true); + notify(device, provisionRequest, TbMsgType.PROVISION_SUCCESS, true); } } catch (InterruptedException | ExecutionException e) { throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); @@ -206,7 +207,7 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { return processCreateDevice(provisionRequest, profile); } - private void notify(Device device, ProvisionRequest provisionRequest, String type, boolean success) { + private void notify(Device device, ProvisionRequest provisionRequest, TbMsgType type, boolean success) { pushProvisionEventToRuleEngine(provisionRequest, device, type); logAction(device.getTenantId(), device.getCustomerId(), device, success, provisionRequest); } @@ -222,14 +223,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { clusterService.onDeviceUpdated(savedDevice, null); saveProvisionStateAttribute(savedDevice).get(); pushDeviceCreatedEventToRuleEngine(savedDevice); - notify(savedDevice, provisionRequest, DataConstants.PROVISION_SUCCESS, true); + notify(savedDevice, provisionRequest, TbMsgType.PROVISION_SUCCESS, true); return new ProvisionResponse(getDeviceCredentials(savedDevice), ProvisionResponseStatus.SUCCESS); } catch (Exception e) { log.warn("[{}] Error during device creation from provision request: [{}]", provisionRequest.getDeviceName(), provisionRequest, e); Device device = deviceService.findDeviceByTenantIdAndName(profile.getTenantId(), provisionRequest.getDeviceName()); if (device != null) { - notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false); + notify(device, provisionRequest, TbMsgType.PROVISION_FAILURE, false); } throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); } @@ -253,7 +254,7 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { return deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()); } - private void pushProvisionEventToRuleEngine(ProvisionRequest request, Device device, String type) { + private void pushProvisionEventToRuleEngine(ProvisionRequest request, Device device, TbMsgType type) { try { JsonNode entityNode = JacksonUtil.valueToTree(request); TbMsg msg = TbMsg.newMsg(type, device.getId(), device.getCustomerId(), createTbMsgMetaData(device), JacksonUtil.toString(entityNode)); @@ -266,10 +267,10 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { private void pushDeviceCreatedEventToRuleEngine(Device device) { try { ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device); - TbMsg msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, device.getId(), device.getCustomerId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + TbMsg msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, device.getId(), device.getCustomerId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); sendToRuleEngine(device.getTenantId(), msg, null); } catch (JsonProcessingException | IllegalArgumentException e) { - log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), DataConstants.ENTITY_CREATED, e); + log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), TbMsgType.ENTITY_CREATED.name(), e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java index 110760acbc..69852e6c05 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.edge; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -23,22 +22,18 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessor; @@ -54,6 +49,8 @@ import org.thingsboard.server.service.edge.rpc.processor.ota.OtaPackageEdgeProce import org.thingsboard.server.service.edge.rpc.processor.queue.QueueEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantProfileEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor; @@ -74,12 +71,6 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { @Autowired private EdgeService edgeService; - @Autowired - private EdgeEventService edgeEventService; - - @Autowired - private TbClusterService clusterService; - @Autowired private EdgeProcessor edgeProcessor; @@ -122,12 +113,21 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { @Autowired private QueueEdgeProcessor queueProcessor; + @Autowired + private TenantEdgeProcessor tenantEdgeProcessor; + + @Autowired + private TenantProfileEdgeProcessor tenantProfileEdgeProcessor; + @Autowired private AlarmEdgeProcessor alarmProcessor; @Autowired private RelationEdgeProcessor relationProcessor; + @Autowired + protected ApplicationEventPublisher eventPublisher; + private ExecutorService dbCallBackExecutor; @PostConstruct @@ -143,37 +143,21 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { } @Override - public Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) throws Exception { + public Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) { edge.setRootRuleChainId(ruleChainId); Edge savedEdge = edgeService.saveEdge(edge); ObjectNode isRootBody = JacksonUtil.newObjectNode(); isRootBody.put(EDGE_IS_ROOT_BODY_KEY, Boolean.TRUE); - saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.RULE_CHAIN, EdgeEventActionType.UPDATED, ruleChainId, isRootBody).get(); + eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).edgeId(edge.getId()).entityId(ruleChainId) + .body(JacksonUtil.toString(isRootBody)).actionType(ActionType.UPDATED).build()); return savedEdge; } - private ListenableFuture saveEdgeEvent(TenantId tenantId, - EdgeId edgeId, - EdgeEventType type, - EdgeEventActionType action, - EntityId entityId, - JsonNode body) { - log.debug("Pushing edge event to edge queue. tenantId [{}], edgeId [{}], type [{}], action[{}], entityId [{}], body [{}]", - tenantId, edgeId, type, action, entityId, body); - - EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(tenantId, edgeId, type, action, entityId, body); - - return Futures.transform(edgeEventService.saveAsync(edgeEvent), unused -> { - clusterService.onEdgeEventUpdate(tenantId, edgeId); - return null; - }, dbCallBackExecutor); - } - @Override public void pushNotificationToEdge(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback) { - log.debug("Pushing notification to edge {}", edgeNotificationMsg); + TenantId tenantId = TenantId.fromUUID(new UUID(edgeNotificationMsg.getTenantIdMSB(), edgeNotificationMsg.getTenantIdLSB())); + log.debug("[{}] Pushing notification to edge {}", tenantId, edgeNotificationMsg); try { - TenantId tenantId = TenantId.fromUUID(new UUID(edgeNotificationMsg.getTenantIdMSB(), edgeNotificationMsg.getTenantIdLSB())); EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); ListenableFuture future; switch (type) { @@ -181,43 +165,43 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { future = edgeProcessor.processEdgeNotification(tenantId, edgeNotificationMsg); break; case ASSET: - future = assetProcessor.processAssetNotification(tenantId, edgeNotificationMsg); + future = assetProcessor.processEntityNotification(tenantId, edgeNotificationMsg); break; case DEVICE: - future = deviceProcessor.processDeviceNotification(tenantId, edgeNotificationMsg); + future = deviceProcessor.processEntityNotification(tenantId, edgeNotificationMsg); break; case ENTITY_VIEW: - future = entityViewProcessor.processEntityViewNotification(tenantId, edgeNotificationMsg); + future = entityViewProcessor.processEntityNotification(tenantId, edgeNotificationMsg); break; case DASHBOARD: - future = dashboardProcessor.processDashboardNotification(tenantId, edgeNotificationMsg); + future = dashboardProcessor.processEntityNotification(tenantId, edgeNotificationMsg); break; case RULE_CHAIN: - future = ruleChainProcessor.processRuleChainNotification(tenantId, edgeNotificationMsg); + future = ruleChainProcessor.processEntityNotification(tenantId, edgeNotificationMsg); break; case USER: - future = userProcessor.processUserNotification(tenantId, edgeNotificationMsg); + future = userProcessor.processEntityNotification(tenantId, edgeNotificationMsg); break; case CUSTOMER: future = customerProcessor.processCustomerNotification(tenantId, edgeNotificationMsg); break; case DEVICE_PROFILE: - future = deviceProfileProcessor.processDeviceProfileNotification(tenantId, edgeNotificationMsg); + future = deviceProfileProcessor.processEntityNotification(tenantId, edgeNotificationMsg); break; case ASSET_PROFILE: - future = assetProfileProcessor.processAssetProfileNotification(tenantId, edgeNotificationMsg); + future = assetProfileProcessor.processEntityNotification(tenantId, edgeNotificationMsg); break; case OTA_PACKAGE: - future = otaPackageProcessor.processOtaPackageNotification(tenantId, edgeNotificationMsg); + future = otaPackageProcessor.processEntityNotification(tenantId, edgeNotificationMsg); break; case WIDGETS_BUNDLE: - future = widgetBundleProcessor.processWidgetsBundleNotification(tenantId, edgeNotificationMsg); + future = widgetBundleProcessor.processEntityNotification(tenantId, edgeNotificationMsg); break; case WIDGET_TYPE: - future = widgetTypeProcessor.processWidgetTypeNotification(tenantId, edgeNotificationMsg); + future = widgetTypeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); break; case QUEUE: - future = queueProcessor.processQueueNotification(tenantId, edgeNotificationMsg); + future = queueProcessor.processEntityNotification(tenantId, edgeNotificationMsg); break; case ALARM: future = alarmProcessor.processAlarmNotification(tenantId, edgeNotificationMsg); @@ -225,8 +209,14 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { case RELATION: future = relationProcessor.processRelationNotification(tenantId, edgeNotificationMsg); break; + case TENANT: + future = tenantEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + break; + case TENANT_PROFILE: + future = tenantProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + break; default: - log.warn("Edge event type [{}] is not designed to be pushed to edge", type); + log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type); future = Futures.immediateFuture(null); } Futures.addCallback(future, new FutureCallback<>() { @@ -237,16 +227,16 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { @Override public void onFailure(Throwable throwable) { - callBackFailure(edgeNotificationMsg, callback, throwable); + callBackFailure(tenantId, edgeNotificationMsg, callback, throwable); } }, dbCallBackExecutor); } catch (Exception e) { - callBackFailure(edgeNotificationMsg, callback, e); + callBackFailure(tenantId, edgeNotificationMsg, callback, e); } } - private void callBackFailure(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback, Throwable throwable) { - log.error("Can't push to edge updates, edgeNotificationMsg [{}]", edgeNotificationMsg, throwable); + private void callBackFailure(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback, Throwable throwable) { + log.error("[{}] Can't push to edge updates, edgeNotificationMsg [{}]", tenantId, edgeNotificationMsg, throwable); callback.onFailure(throwable); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 474c7ba233..dc83e0f2a2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -35,7 +35,10 @@ import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; @@ -55,6 +58,8 @@ import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgePr import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.settings.AdminSettingsEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.telemetry.TelemetryEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantProfileEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor; @@ -113,6 +118,9 @@ public class EdgeContextComponent { @Autowired private CustomerService customerService; + @Autowired + private WidgetTypeService widgetTypeService; + @Autowired private WidgetsBundleService widgetsBundleService; @@ -122,6 +130,12 @@ public class EdgeContextComponent { @Autowired private OtaPackageService otaPackageService; + @Autowired + private TenantService tenantService; + + @Autowired + private TenantProfileService tenantProfileService; + @Autowired private QueueService queueService; @@ -179,6 +193,12 @@ public class EdgeContextComponent { @Autowired private QueueEdgeProcessor queueEdgeProcessor; + @Autowired + private TenantEdgeProcessor tenantEdgeProcessor; + + @Autowired + private TenantProfileEdgeProcessor tenantProfileEdgeProcessor; + @Autowired private EdgeMsgConstructor edgeMsgConstructor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java new file mode 100644 index 0000000000..43b05094a4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -0,0 +1,158 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; +import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; + +import javax.annotation.PostConstruct; + +import static org.thingsboard.server.service.entitiy.DefaultTbNotificationEntityService.edgeTypeByActionType; + + +/** + * This event listener does not support async event processing because relay on ThreadLocal + * Another possible approach is to implement a special annotation and a bunch of classes similar to TransactionalApplicationListener + * This class is the simplest approach to maintain edge synchronization within the single class. + *

+ * For async event publishers, you have to decide whether publish event on creating async task in the same thread where dao method called + * @Autowired + * EdgeEventSynchronizationManager edgeSynchronizationManager + * ... + * //some async write action make future + * if (!edgeSynchronizationManager.isSync()) { + * future.addCallback(eventPublisher.publishEvent(...)) + * } + * */ +@Component +@RequiredArgsConstructor +@Slf4j +public class EdgeEventSourcingListener { + + private final TbClusterService tbClusterService; + private final EdgeSynchronizationManager edgeSynchronizationManager; + + @PostConstruct + public void init() { + log.info("EdgeEventSourcingListener initiated"); + } + + @TransactionalEventListener(fallbackExecution = true) + public void handleEvent(SaveEntityEvent event) { + if (edgeSynchronizationManager.isSync()) { + return; + } + try { + if (!isValidEdgeEventEntity(event.getEntity())) { + return; + } + log.trace("[{}] SaveEntityEvent called: {}", event.getTenantId(), event); + EdgeEventActionType action = Boolean.TRUE.equals(event.getAdded()) ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED; + tbClusterService.sendNotificationMsgToEdge(event.getTenantId(), null, event.getEntityId(), + null, null, action); + } catch (Exception e) { + log.error("[{}] failed to process SaveEntityEvent: {}", event.getTenantId(), event); + } + } + + @TransactionalEventListener(fallbackExecution = true) + public void handleEvent(DeleteEntityEvent event) { + if (edgeSynchronizationManager.isSync()) { + return; + } + try { + log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event); + tbClusterService.sendNotificationMsgToEdge(event.getTenantId(), event.getEdgeId(), event.getEntityId(), + JacksonUtil.toString(event.getEntity()), null, EdgeEventActionType.DELETED); + } catch (Exception e) { + log.error("[{}] failed to process DeleteEntityEvent: {}", event.getTenantId(), event); + } + } + + @TransactionalEventListener(fallbackExecution = true) + public void handleEvent(ActionEntityEvent event) { + if (edgeSynchronizationManager.isSync()) { + return; + } + try { + log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); + tbClusterService.sendNotificationMsgToEdge(event.getTenantId(), event.getEdgeId(), event.getEntityId(), + event.getBody(), null, edgeTypeByActionType(event.getActionType())); + } catch (Exception e) { + log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event); + } + } + + @TransactionalEventListener(fallbackExecution = true) + public void handleEvent(RelationActionEvent event) { + if (edgeSynchronizationManager.isSync()) { + return; + } + try { + EntityRelation relation = event.getRelation(); + if (relation == null) { + log.trace("[{}] skipping RelationActionEvent event in case relation is null: {}", event.getTenantId(), event); + return; + } + if (!RelationTypeGroup.COMMON.equals(relation.getTypeGroup())) { + log.trace("[{}] skipping RelationActionEvent event in case NOT COMMON relation type group: {}", event.getTenantId(), event); + return; + } + log.trace("[{}] RelationActionEvent called: {}", event.getTenantId(), event); + tbClusterService.sendNotificationMsgToEdge(event.getTenantId(), null, null, + JacksonUtil.toString(relation), EdgeEventType.RELATION, edgeTypeByActionType(event.getActionType())); + } catch (Exception e) { + log.error("[{}] failed to process RelationActionEvent: {}", event.getTenantId(), event); + } + } + + private boolean isValidEdgeEventEntity(Object entity) { + if (entity instanceof OtaPackageInfo) { + OtaPackageInfo otaPackageInfo = (OtaPackageInfo) entity; + return otaPackageInfo.hasUrl() || otaPackageInfo.isHasData(); + } else if (entity instanceof RuleChain) { + RuleChain ruleChain = (RuleChain) entity; + return RuleChainType.EDGE.equals(ruleChain.getType()); + } else if (entity instanceof User) { + User user = (User) entity; + return !Authority.SYS_ADMIN.equals(user.getAuthority()); + } else if (entity instanceof AlarmApiCallResult) { + AlarmApiCallResult alarmApiCallResult = (AlarmApiCallResult) entity; + return alarmApiCallResult.isModified(); + } + // Default: If the entity doesn't match any of the conditions, consider it as valid. + return true; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index e7214177d2..50b80a82f7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -71,10 +72,6 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; -import static org.thingsboard.server.common.data.DataConstants.CONNECT_EVENT; -import static org.thingsboard.server.common.data.DataConstants.DISCONNECT_EVENT; -import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; - @Service @Slf4j @ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") @@ -198,17 +195,17 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i switch (msg.getMsgType()) { case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG: EdgeEventUpdateMsg edgeEventUpdateMsg = (EdgeEventUpdateMsg) msg; - log.trace("[{}] onToEdgeSessionMsg [{}]", edgeEventUpdateMsg.getTenantId(), msg); + log.trace("[{}] onToEdgeSessionMsg [{}]", tenantId, msg); onEdgeEvent(tenantId, edgeEventUpdateMsg.getEdgeId()); break; case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG: ToEdgeSyncRequest toEdgeSyncRequest = (ToEdgeSyncRequest) msg; - log.trace("[{}] toEdgeSyncRequest [{}]", toEdgeSyncRequest.getTenantId(), msg); + log.trace("[{}] toEdgeSyncRequest [{}]", tenantId, msg); startSyncProcess(tenantId, toEdgeSyncRequest.getEdgeId(), toEdgeSyncRequest.getId()); break; case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG: FromEdgeSyncResponse fromEdgeSyncResponse = (FromEdgeSyncResponse) msg; - log.trace("[{}] fromEdgeSyncResponse [{}]", fromEdgeSyncResponse.getTenantId(), msg); + log.trace("[{}] fromEdgeSyncResponse [{}]", tenantId, msg); processSyncResponse(fromEdgeSyncResponse); break; } @@ -266,7 +263,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } private void onEdgeConnect(EdgeId edgeId, EdgeGrpcSession edgeGrpcSession) { - log.info("[{}] edge [{}] connected successfully.", edgeGrpcSession.getSessionId(), edgeId); + TenantId tenantId = edgeGrpcSession.getEdge().getTenantId(); + log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId); sessions.put(edgeId, edgeGrpcSession); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); newEventLock.lock(); @@ -275,10 +273,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } finally { newEventLock.unlock(); } - save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true); + save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true); long lastConnectTs = System.currentTimeMillis(); - save(edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs); - pushRuleEngineMessage(edgeGrpcSession.getEdge().getTenantId(), edgeId, lastConnectTs, CONNECT_EVENT); + save(tenantId, edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs); + pushRuleEngineMessage(tenantId, edgeId, lastConnectTs, TbMsgType.CONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); scheduleEdgeEventsCheck(edgeGrpcSession); } @@ -337,7 +335,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i newEventLock.lock(); try { if (Boolean.TRUE.equals(sessionNewEvents.get(edgeId))) { - log.trace("[{}] Set session new events flag to false", edgeId.getId()); + log.trace("[{}][{}] Set session new events flag to false", tenantId, edgeId.getId()); sessionNewEvents.put(edgeId, false); Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() { @Override @@ -395,46 +393,49 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } finally { newEventLock.unlock(); } - save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false); + TenantId tenantId = toRemove.getEdge().getTenantId(); + save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false); long lastDisconnectTs = System.currentTimeMillis(); - save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs); - pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edgeId, lastDisconnectTs, DISCONNECT_EVENT); + save(tenantId, edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs); + pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edgeId, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); } else { log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId); } } - private void save(EdgeId edgeId, String key, long value) { - log.debug("[{}] Updating long edge telemetry [{}] [{}]", edgeId, key, value); + private void save(TenantId tenantId, EdgeId edgeId, String key, long value) { + log.debug("[{}][{}] Updating long edge telemetry [{}] [{}]", tenantId, edgeId, key, value); if (persistToTelemetry) { tsSubService.saveAndNotify( - TenantId.SYS_TENANT_ID, edgeId, + tenantId, edgeId, Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(key, value))), - new AttributeSaveCallback(edgeId, key, value)); + new AttributeSaveCallback(tenantId, edgeId, key, value)); } else { - tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, edgeId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(edgeId, key, value)); + tsSubService.saveAttrAndNotify(tenantId, edgeId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(tenantId, edgeId, key, value)); } } - private void save(EdgeId edgeId, String key, boolean value) { - log.debug("[{}] Updating boolean edge telemetry [{}] [{}]", edgeId, key, value); + private void save(TenantId tenantId, EdgeId edgeId, String key, boolean value) { + log.debug("[{}][{}] Updating boolean edge telemetry [{}] [{}]", tenantId, edgeId, key, value); if (persistToTelemetry) { tsSubService.saveAndNotify( - TenantId.SYS_TENANT_ID, edgeId, + tenantId, edgeId, Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), - new AttributeSaveCallback(edgeId, key, value)); + new AttributeSaveCallback(tenantId, edgeId, key, value)); } else { - tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, edgeId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(edgeId, key, value)); + tsSubService.saveAttrAndNotify(tenantId, edgeId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(tenantId, edgeId, key, value)); } } private static class AttributeSaveCallback implements FutureCallback { + private final TenantId tenantId; private final EdgeId edgeId; private final String key; private final Object value; - AttributeSaveCallback(EdgeId edgeId, String key, Object value) { + AttributeSaveCallback(TenantId tenantId, EdgeId edgeId, String key, Object value) { + this.tenantId = tenantId; this.edgeId = edgeId; this.key = key; this.value = value; @@ -442,19 +443,19 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i @Override public void onSuccess(@Nullable Void result) { - log.trace("[{}] Successfully updated attribute [{}] with value [{}]", edgeId, key, value); + log.trace("[{}][{}] Successfully updated attribute [{}] with value [{}]", tenantId, edgeId, key, value); } @Override public void onFailure(Throwable t) { - log.warn("[{}] Failed to update attribute [{}] with value [{}]", edgeId, key, value, t); + log.warn("[{}][{}] Failed to update attribute [{}] with value [{}]", tenantId, edgeId, key, value, t); } } - private void pushRuleEngineMessage(TenantId tenantId, EdgeId edgeId, long ts, String msgType) { + private void pushRuleEngineMessage(TenantId tenantId, EdgeId edgeId, long ts, TbMsgType msgType) { try { ObjectNode edgeState = JacksonUtil.newObjectNode(); - if (msgType.equals(CONNECT_EVENT)) { + if (msgType.equals(TbMsgType.CONNECT_EVENT)) { edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, true); edgeState.put(DefaultDeviceStateService.LAST_CONNECT_TIME, ts); } else { @@ -464,7 +465,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i String data = JacksonUtil.toString(edgeState); TbMsgMetaData md = new TbMsgMetaData(); if (!persistToTelemetry) { - md.putValue(DataConstants.SCOPE, SERVER_SCOPE); + md.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE); } TbMsg tbMsg = TbMsg.newMsg(msgType, edgeId, md, TbMsgDataType.JSON, data); clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 4f0f5d277a..fd341365cd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -39,12 +39,16 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectResponseCode; import org.thingsboard.server.gen.edge.v1.ConnectResponseMsg; +import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; @@ -53,6 +57,7 @@ import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; import org.thingsboard.server.gen.edge.v1.EdgeUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityDataProto; +import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; @@ -100,6 +105,7 @@ public final class EdgeGrpcSession implements Closeable { private EdgeContextComponent ctx; private Edge edge; + private TenantId tenantId; private StreamObserver inputStream; private StreamObserver outputStream; private boolean connected; @@ -143,7 +149,7 @@ public final class EdgeGrpcSession implements Closeable { outputStream.onError(new RuntimeException(responseMsg.getErrorMsg())); } else { if (requestMsg.getConnectRequestMsg().hasMaxInboundMessageSize()) { - log.debug("[{}] Client max inbound message size: {}", sessionId, requestMsg.getConnectRequestMsg().getMaxInboundMessageSize()); + log.debug("[{}][{}] Client max inbound message size: {}", tenantId, sessionId, requestMsg.getConnectRequestMsg().getMaxInboundMessageSize()); clientMaxInboundMessageSize = requestMsg.getConnectRequestMsg().getMaxInboundMessageSize(); } connected = true; @@ -151,8 +157,8 @@ public final class EdgeGrpcSession implements Closeable { } if (connected) { if (requestMsg.getMsgType().equals(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE)) { - if (requestMsg.hasSyncRequestMsg() && requestMsg.getSyncRequestMsg().getSyncRequired()) { - boolean fullSync = true; + if (requestMsg.hasSyncRequestMsg()) { + boolean fullSync = false; if (requestMsg.getSyncRequestMsg().hasFullSync()) { fullSync = requestMsg.getSyncRequestMsg().getFullSync(); } @@ -174,13 +180,13 @@ public final class EdgeGrpcSession implements Closeable { @Override public void onError(Throwable t) { - log.error("[{}] Stream was terminated due to error:", sessionId, t); + log.error("[{}][{}] Stream was terminated due to error:", tenantId, sessionId, t); closeSession(); } @Override public void onCompleted() { - log.info("[{}] Stream was closed and completed successfully!", sessionId); + log.info("[{}][{}] Stream was closed and completed successfully!", tenantId, sessionId); closeSession(); } @@ -201,7 +207,7 @@ public final class EdgeGrpcSession implements Closeable { } public void startSyncProcess(boolean fullSync) { - log.trace("[{}][{}][{}] Staring edge sync process", edge.getTenantId(), edge.getId(), this.sessionId); + log.trace("[{}][{}][{}] Staring edge sync process", this.tenantId, edge.getId(), this.sessionId); syncCompleted = false; interruptGeneralProcessingOnSync(); doSync(new EdgeSyncCursor(ctx, edge, fullSync)); @@ -211,7 +217,7 @@ public final class EdgeGrpcSession implements Closeable { if (cursor.hasNext()) { EdgeEventFetcher next = cursor.getNext(); log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}", - edge.getTenantId(), edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); + this.tenantId, edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); ListenableFuture> future = startProcessingEdgeEvents(next); Futures.addCallback(future, new FutureCallback<>() { @Override @@ -221,7 +227,7 @@ public final class EdgeGrpcSession implements Closeable { @Override public void onFailure(Throwable t) { - log.error("[{}][{}] Exception during sync process", edge.getTenantId(), edge.getId(), t); + log.error("[{}][{}] Exception during sync process", tenantId, edge.getId(), t); } }, ctx.getGrpcCallbackExecutorService()); } else { @@ -238,7 +244,7 @@ public final class EdgeGrpcSession implements Closeable { @Override public void onFailure(Throwable t) { - log.error("[{}][{}] Exception during sending sync complete", edge.getTenantId(), edge.getId(), t); + log.error("[{}][{}] Exception during sending sync complete", tenantId, edge.getId(), t); } }, ctx.getGrpcCallbackExecutorService()); } @@ -274,39 +280,40 @@ public final class EdgeGrpcSession implements Closeable { try { if (msg.getSuccess()) { sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); - log.debug("[{}] Msg has been processed successfully!Msd Id: [{}], Msg: {}", edge.getRoutingKey(), msg.getDownlinkMsgId(), msg); + log.debug("[{}][{}] Msg has been processed successfully!Msd Id: [{}], Msg: {}", this.tenantId, edge.getRoutingKey(), msg.getDownlinkMsgId(), msg); } else { - log.error("[{}] Msg processing failed! Msd Id: [{}], Error msg: {}", edge.getRoutingKey(), msg.getDownlinkMsgId(), msg.getErrorMsg()); + log.error("[{}][{}] Msg processing failed! Msd Id: [{}], Error msg: {}", this.tenantId, edge.getRoutingKey(), msg.getDownlinkMsgId(), msg.getErrorMsg()); } if (sessionState.getPendingMsgsMap().isEmpty()) { - log.debug("[{}] Pending msgs map is empty. Stopping current iteration", edge.getRoutingKey()); + log.debug("[{}][{}] Pending msgs map is empty. Stopping current iteration", this.tenantId, edge.getRoutingKey()); stopCurrentSendDownlinkMsgsTask(false); } } catch (Exception e) { - log.error("[{}] Can't process downlink response message [{}]", this.sessionId, msg, e); + log.error("[{}][{}] Can't process downlink response message [{}]", this.tenantId, this.sessionId, msg, e); } } private void sendDownlinkMsg(ResponseMsg downlinkMsg) { - log.trace("[{}] Sending downlink msg [{}]", this.sessionId, downlinkMsg); + log.trace("[{}][{}] Sending downlink msg [{}]", this.tenantId, this.sessionId, downlinkMsg); if (isConnected()) { downlinkMsgLock.lock(); try { outputStream.onNext(downlinkMsg); } catch (Exception e) { - log.error("[{}] Failed to send downlink message [{}]", this.sessionId, downlinkMsg, e); + log.error("[{}][{}] Failed to send downlink message [{}]", this.tenantId, this.sessionId, downlinkMsg, e); connected = false; sessionCloseListener.accept(edge.getId(), sessionId); } finally { downlinkMsgLock.unlock(); } - log.trace("[{}] Response msg successfully sent [{}]", this.sessionId, downlinkMsg); + log.trace("[{}][{}] Response msg successfully sent [{}]", this.tenantId, this.sessionId, downlinkMsg); } } void onConfigurationUpdate(Edge edge) { log.debug("[{}] onConfigurationUpdate [{}]", this.sessionId, edge); this.edge = edge; + this.tenantId = edge.getTenantId(); EdgeUpdateMsg edgeConfig = EdgeUpdateMsg.newBuilder() .setConfiguration(ctx.getEdgeMsgConstructor().constructEdgeConfiguration(edge)).build(); ResponseMsg edgeConfigMsg = ResponseMsg.newBuilder() @@ -317,7 +324,7 @@ public final class EdgeGrpcSession implements Closeable { ListenableFuture processEdgeEvents() throws Exception { SettableFuture result = SettableFuture.create(); - log.trace("[{}] starting processing edge events", this.sessionId); + log.trace("[{}][{}] starting processing edge events", this.tenantId, this.sessionId); if (isConnected() && isSyncCompleted()) { Pair startTsAndSeqId = getQueueStartTsAndSeqId().get(); this.previousStartTs = startTsAndSeqId.getFirst(); @@ -337,7 +344,7 @@ public final class EdgeGrpcSession implements Closeable { Futures.addCallback(updateFuture, new FutureCallback<>() { @Override public void onSuccess(@Nullable List list) { - log.debug("[{}] queue offset was updated [{}]", sessionId, newStartTsAndSeqId); + log.debug("[{}][{}] queue offset was updated [{}]", tenantId, sessionId, newStartTsAndSeqId); if (fetcher.isSeqIdNewCycleStarted()) { seqIdEnd = fetcher.getSeqIdEnd(); boolean newEventsAvailable = isNewEdgeEventsAvailable(); @@ -354,24 +361,24 @@ public final class EdgeGrpcSession implements Closeable { @Override public void onFailure(Throwable t) { - log.error("[{}] Failed to update queue offset [{}]", sessionId, newStartTsAndSeqId, t); + log.error("[{}][{}] Failed to update queue offset [{}]", tenantId, sessionId, newStartTsAndSeqId, t); result.setException(t); } }, ctx.getGrpcCallbackExecutorService()); } else { - log.trace("[{}] newStartTsAndSeqId is null. Skipping iteration without db update", sessionId); + log.trace("[{}][{}] newStartTsAndSeqId is null. Skipping iteration without db update", tenantId, sessionId); result.set(null); } } @Override public void onFailure(Throwable t) { - log.error("[{}] Failed to process events", sessionId, t); + log.error("[{}][{}] Failed to process events", tenantId, sessionId, t); result.setException(t); } }, ctx.getGrpcCallbackExecutorService()); } else { - log.trace("[{}] edge is not connected or sync is not completed. Skipping iteration", sessionId); + log.trace("[{}][{}] edge is not connected or sync is not completed. Skipping iteration", tenantId, sessionId); result.set(null); } return result; @@ -388,13 +395,13 @@ public final class EdgeGrpcSession implements Closeable { try { PageData pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); if (isConnected() && !pageData.getData().isEmpty()) { - log.trace("[{}] [{}] event(s) are going to be processed.", this.sessionId, pageData.getData().size()); + log.trace("[{}][{}][{}] event(s) are going to be processed.", this.tenantId, this.sessionId, pageData.getData().size()); List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { @Override public void onSuccess(@Nullable Boolean isInterrupted) { if (Boolean.TRUE.equals(isInterrupted)) { - log.debug("[{}][{}][{}] Send downlink messages task was interrupted", edge.getTenantId(), edge.getId(), sessionId); + log.debug("[{}][{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId(), sessionId); result.set(null); } else { if (isConnected() && pageData.hasNext()) { @@ -447,14 +454,14 @@ public final class EdgeGrpcSession implements Closeable { if (isConnected() && sessionState.getPendingMsgsMap().values().size() > 0) { List copy = new ArrayList<>(sessionState.getPendingMsgsMap().values()); if (attempt > 1) { - log.warn("[{}] Failed to deliver the batch: {}, attempt: {}", this.sessionId, copy, attempt); + log.warn("[{}][{}] Failed to deliver the batch: {}, attempt: {}", this.tenantId, this.sessionId, copy, attempt); } - log.trace("[{}] [{}] downlink msg(s) are going to be send.", this.sessionId, copy.size()); + log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", this.tenantId, this.sessionId, copy.size()); for (DownlinkMsg downlinkMsg : copy) { if (this.clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > this.clientMaxInboundMessageSize) { log.error("[{}][{}][{}] Downlink msg size [{}] exceeds client max inbound message size [{}]. Skipping this message. " + "Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it." + - "Message {}", edge.getTenantId(), edge.getId(), this.sessionId, downlinkMsg.getSerializedSize(), + "Message {}", this.tenantId, edge.getId(), this.sessionId, downlinkMsg.getSerializedSize(), this.clientMaxInboundMessageSize, downlinkMsg); sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId()); } else { @@ -466,15 +473,15 @@ public final class EdgeGrpcSession implements Closeable { if (attempt < MAX_DOWNLINK_ATTEMPTS) { scheduleDownlinkMsgsPackSend(attempt + 1); } else { - log.warn("[{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", - this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy); + log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", + this.tenantId, this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy); stopCurrentSendDownlinkMsgsTask(false); } } else { stopCurrentSendDownlinkMsgsTask(false); } } catch (Exception e) { - log.warn("[{}] Failed to send downlink msgs. Error msg {}", this.sessionId, e.getMessage(), e); + log.warn("[{}][{}] Failed to send downlink msgs. Error msg {}", this.tenantId, this.sessionId, e.getMessage(), e); stopCurrentSendDownlinkMsgsTask(true); } }; @@ -494,7 +501,7 @@ public final class EdgeGrpcSession implements Closeable { private List convertToDownlinkMsgsPack(List edgeEvents) { List result = new ArrayList<>(); for (EdgeEvent edgeEvent : edgeEvents) { - log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent); + log.trace("[{}][{}] converting edge event to downlink msg [{}]", this.tenantId, this.sessionId, edgeEvent); DownlinkMsg downlinkMsg = null; try { switch (edgeEvent.getAction()) { @@ -513,7 +520,7 @@ public final class EdgeGrpcSession implements Closeable { case ASSIGNED_TO_CUSTOMER: case UNASSIGNED_FROM_CUSTOMER: downlinkMsg = convertEntityEventToDownlink(edgeEvent); - log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg); + log.trace("[{}][{}] entity message processed [{}]", this.tenantId, this.sessionId, downlinkMsg); break; case ATTRIBUTES_UPDATED: case POST_ATTRIBUTES: @@ -522,10 +529,10 @@ public final class EdgeGrpcSession implements Closeable { downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edgeEvent); break; default: - log.warn("[{}][{}] Unsupported action type [{}]", edge.getTenantId(), this.sessionId, edgeEvent.getAction()); + log.warn("[{}][{}] Unsupported action type [{}]", this.tenantId, this.sessionId, edgeEvent.getAction()); } } catch (Exception e) { - log.error("[{}][{}] Exception during converting edge event to downlink msg", edge.getTenantId(), this.sessionId, e); + log.error("[{}][{}] Exception during converting edge event to downlink msg", this.tenantId, this.sessionId, e); } if (downlinkMsg != null) { result.add(downlinkMsg); @@ -561,7 +568,7 @@ public final class EdgeGrpcSession implements Closeable { PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), 0L, this.previousStartSeqId == 0 ? null : this.previousStartSeqId - 1, pageLink); return !edgeEvents.getData().isEmpty(); } catch (Exception e) { - log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", edge.getTenantId(), edge.getId(), sessionId, e); + log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", this.tenantId, edge.getId(), sessionId, e); } return false; } @@ -572,7 +579,7 @@ public final class EdgeGrpcSession implements Closeable { PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), this.newStartSeqId, null, pageLink); return !edgeEvents.getData().isEmpty(); } catch (Exception e) { - log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", edge.getTenantId(), edge.getId(), sessionId, e); + log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", this.tenantId, edge.getId(), sessionId, e); } return false; } @@ -586,7 +593,7 @@ public final class EdgeGrpcSession implements Closeable { startSeqId = edgeEvents.getData().get(0).getSeqId() - 1; } } catch (Exception e) { - log.error("[{}][{}][{}] Failed to execute findStartSeqIdFromOldestEventIfAny", edge.getTenantId(), edge.getId(), sessionId, e); + log.error("[{}][{}][{}] Failed to execute findStartSeqIdFromOldestEventIfAny", this.tenantId, edge.getId(), sessionId, e); } return startSeqId; } @@ -602,7 +609,7 @@ public final class EdgeGrpcSession implements Closeable { } private DownlinkMsg convertEntityEventToDownlink(EdgeEvent edgeEvent) { - log.trace("Executing convertEntityEventToDownlink, edgeEvent [{}], action [{}]", edgeEvent, edgeEvent.getAction()); + log.trace("[{}] Executing convertEntityEventToDownlink, edgeEvent [{}], action [{}]", this.tenantId, edgeEvent, edgeEvent.getAction()); switch (edgeEvent.getType()) { case EDGE: return ctx.getEdgeProcessor().convertEdgeEventToDownlink(edgeEvent); @@ -640,8 +647,12 @@ public final class EdgeGrpcSession implements Closeable { return ctx.getOtaPackageEdgeProcessor().convertOtaPackageEventToDownlink(edgeEvent); case QUEUE: return ctx.getQueueEdgeProcessor().convertQueueEventToDownlink(edgeEvent); + case TENANT: + return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent); + case TENANT_PROFILE: + return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent); default: - log.warn("Unsupported edge event type [{}]", edgeEvent); + log.warn("[{}] Unsupported edge event type [{}]", this.tenantId, edgeEvent); return null; } } @@ -654,6 +665,11 @@ public final class EdgeGrpcSession implements Closeable { result.addAll(ctx.getTelemetryProcessor().processTelemetryMsg(edge.getTenantId(), entityData)); } } + if (uplinkMsg.getDeviceProfileUpdateMsgCount() > 0) { + for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : uplinkMsg.getDeviceProfileUpdateMsgList()) { + result.add(ctx.getDeviceProfileProcessor().processDeviceProfileMsgFromEdge(edge.getTenantId(), edge, deviceProfileUpdateMsg)); + } + } if (uplinkMsg.getDeviceUpdateMsgCount() > 0) { for (DeviceUpdateMsg deviceUpdateMsg : uplinkMsg.getDeviceUpdateMsgList()) { result.add(ctx.getDeviceProcessor().processDeviceMsgFromEdge(edge.getTenantId(), edge, deviceUpdateMsg)); @@ -664,16 +680,36 @@ public final class EdgeGrpcSession implements Closeable { result.add(ctx.getDeviceProcessor().processDeviceCredentialsMsg(edge.getTenantId(), deviceCredentialsUpdateMsg)); } } + if (uplinkMsg.getAssetProfileUpdateMsgCount() > 0) { + for (AssetProfileUpdateMsg assetProfileUpdateMsg : uplinkMsg.getAssetProfileUpdateMsgList()) { + result.add(ctx.getAssetProfileProcessor().processAssetProfileMsgFromEdge(edge.getTenantId(), edge, assetProfileUpdateMsg)); + } + } + if (uplinkMsg.getAssetUpdateMsgCount() > 0) { + for (AssetUpdateMsg assetUpdateMsg : uplinkMsg.getAssetUpdateMsgList()) { + result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg)); + } + } if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { result.add(ctx.getAlarmProcessor().processAlarmMsg(edge.getTenantId(), alarmUpdateMsg)); } } + if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) { + for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) { + result.add(ctx.getEntityViewProcessor().processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg)); + } + } if (uplinkMsg.getRelationUpdateMsgCount() > 0) { for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) { result.add(ctx.getRelationProcessor().processRelationMsg(edge.getTenantId(), relationUpdateMsg)); } } + if (uplinkMsg.getDashboardUpdateMsgCount() > 0) { + for (DashboardUpdateMsg dashboardUpdateMsg : uplinkMsg.getDashboardUpdateMsgList()) { + result.add(ctx.getDashboardProcessor().processDashboardMsgFromEdge(edge.getTenantId(), edge, dashboardUpdateMsg)); + } + } if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) { for (RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg : uplinkMsg.getRuleChainMetadataRequestMsgList()) { result.add(ctx.getEdgeRequestsService().processRuleChainMetadataRequestMsg(edge.getTenantId(), edge, ruleChainMetadataRequestMsg)); @@ -715,7 +751,7 @@ public final class EdgeGrpcSession implements Closeable { } } } catch (Exception e) { - log.error("[{}] Can't process uplink msg [{}]", this.sessionId, uplinkMsg, e); + log.error("[{}][{}] Can't process uplink msg [{}]", this.tenantId, this.sessionId, uplinkMsg, e); return Futures.immediateFailedFuture(e); } return Futures.allAsList(result); @@ -726,6 +762,7 @@ public final class EdgeGrpcSession implements Closeable { Optional optional = ctx.getEdgeService().findEdgeByRoutingKey(TenantId.SYS_TENANT_ID, request.getEdgeRoutingKey()); if (optional.isPresent()) { edge = optional.get(); + this.tenantId = edge.getTenantId(); try { if (edge.getSecret().equals(request.getEdgeSecret())) { sessionOpenListener.accept(edge.getId(), this); @@ -757,25 +794,25 @@ public final class EdgeGrpcSession implements Closeable { @Override public void close() { - log.debug("[{}] Closing session", sessionId); + log.debug("[{}][{}] Closing session", this.tenantId, sessionId); connected = false; try { outputStream.onCompleted(); } catch (Exception e) { - log.debug("[{}] Failed to close output stream: {}", sessionId, e.getMessage()); + log.debug("[{}][{}] Failed to close output stream: {}", this.tenantId, sessionId, e.getMessage()); } } private void interruptPreviousSendDownlinkMsgsTask() { if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone() || sessionState.getScheduledSendDownlinkTask() != null && !sessionState.getScheduledSendDownlinkTask().isCancelled()) { - log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId); + log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", this.tenantId, edge.getId(), this.sessionId); stopCurrentSendDownlinkMsgsTask(true); } } private void interruptGeneralProcessingOnSync() { - log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", edge.getTenantId(), edge.getId(), this.sessionId); + log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", this.tenantId, edge.getId(), this.sessionId); stopCurrentSendDownlinkMsgsTask(true); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index bf3e804f7e..a39dae3b54 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.service.edge.rpc.fetch.AssetsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerUsersEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.DashboardsEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.DefaultProfilesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.DeviceProfilesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.DevicesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.EdgeEventFetcher; @@ -32,8 +33,11 @@ import org.thingsboard.server.service.edge.rpc.fetch.EntityViewsEdgeEventFetcher import org.thingsboard.server.service.edge.rpc.fetch.OtaPackagesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.QueuesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.RuleChainsEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.SystemWidgetTypesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.SystemWidgetsBundlesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.TenantAdminUsersEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.TenantEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.TenantWidgetTypesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.TenantWidgetsBundlesEdgeEventFetcher; import java.util.LinkedList; @@ -51,8 +55,7 @@ public class EdgeSyncCursor { fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService())); fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), ctx.getFreemarkerConfig())); - fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); - fetchers.add(new AssetProfilesEdgeEventFetcher(ctx.getAssetProfileService())); + fetchers.add(new TenantEdgeEventFetcher(ctx.getTenantService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); @@ -61,11 +64,16 @@ public class EdgeSyncCursor { fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); } } + fetchers.add(new DashboardsEdgeEventFetcher(ctx.getDashboardService())); + fetchers.add(new DefaultProfilesEdgeEventFetcher(ctx.getDeviceProfileService(), ctx.getAssetProfileService())); + fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); + fetchers.add(new AssetProfilesEdgeEventFetcher(ctx.getAssetProfileService())); fetchers.add(new DevicesEdgeEventFetcher(ctx.getDeviceService())); fetchers.add(new AssetsEdgeEventFetcher(ctx.getAssetService())); fetchers.add(new EntityViewsEdgeEventFetcher(ctx.getEntityViewService())); - fetchers.add(new DashboardsEdgeEventFetcher(ctx.getDashboardService())); if (fullSync) { + fetchers.add(new SystemWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); + fetchers.add(new TenantWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); fetchers.add(new SystemWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); fetchers.add(new OtaPackagesEdgeEventFetcher(ctx.getOtaPackageService())); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java index 447a73e5cf..15d7c27ff6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java @@ -15,20 +15,9 @@ */ package org.thingsboard.server.service.edge.rpc.constructor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EntityViewId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -37,37 +26,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; @TbCoreComponent public class AlarmMsgConstructor { - @Autowired - private DeviceService deviceService; - - @Autowired - private AssetService assetService; - - @Autowired - private EntityViewService entityViewService; - - public AlarmUpdateMsg constructAlarmUpdatedMsg(TenantId tenantId, UpdateMsgType msgType, Alarm alarm) { - String entityName = null; - switch (alarm.getOriginator().getEntityType()) { - case DEVICE: - Device deviceById = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId())); - if (deviceById != null) { - entityName = deviceById.getName(); - } - break; - case ASSET: - Asset assetById = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId())); - if (assetById != null) { - entityName = assetById.getName(); - } - break; - case ENTITY_VIEW: - EntityView entityViewById = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId())); - if (entityViewById != null) { - entityName = entityViewById.getName(); - } - break; - } + public AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm, String entityName) { AlarmUpdateMsg.Builder builder = AlarmUpdateMsg.newBuilder() .setMsgType(msgType) .setIdMSB(alarm.getId().getId().getMostSignificantBits()) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java index edfdefb603..50c2b46891 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java @@ -33,10 +33,17 @@ public class DashboardMsgConstructor { .setIdMSB(dashboard.getId().getId().getMostSignificantBits()) .setIdLSB(dashboard.getId().getId().getLeastSignificantBits()) .setTitle(dashboard.getTitle()) - .setConfiguration(JacksonUtil.toString(dashboard.getConfiguration())); + .setConfiguration(JacksonUtil.toString(dashboard.getConfiguration())) + .setMobileHide(dashboard.isMobileHide()); if (dashboard.getAssignedCustomers() != null) { builder.setAssignedCustomers(JacksonUtil.toString(dashboard.getAssignedCustomers())); } + if (dashboard.getImage() != null) { + builder.setImage(dashboard.getImage()); + } + if (dashboard.getMobileOrder() != null) { + builder.setMobileOrder(dashboard.getMobileOrder()); + } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java index 242a15ba56..47dbad231d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; import org.thingsboard.server.gen.edge.v1.EntityDataProto; @@ -40,7 +41,7 @@ import java.util.List; @TbCoreComponent public class EntityDataMsgConstructor { - public EntityDataProto constructEntityDataMsg(EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) { + public EntityDataProto constructEntityDataMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) { EntityDataProto.Builder builder = EntityDataProto.newBuilder() .setEntityIdMSB(entityId.getId().getMostSignificantBits()) .setEntityIdLSB(entityId.getId().getLeastSignificantBits()) @@ -57,7 +58,7 @@ public class EntityDataMsgConstructor { } builder.setPostTelemetryMsg(JsonConverter.convertToTelemetryProto(data.getAsJsonObject("data"), ts)); } catch (Exception e) { - log.warn("[{}] Can't convert to telemetry proto, entityData [{}]", entityId, entityData, e); + log.warn("[{}][{}] Can't convert to telemetry proto, entityData [{}]", tenantId, entityId, entityData, e); } break; case ATTRIBUTES_UPDATED: @@ -67,7 +68,7 @@ public class EntityDataMsgConstructor { builder.setAttributesUpdatedMsg(attributesUpdatedMsg); builder.setPostAttributeScope(getScopeOfDefault(data)); } catch (Exception e) { - log.warn("[{}] Can't convert to AttributesUpdatedMsg proto, entityData [{}]", entityId, entityData, e); + log.warn("[{}][{}] Can't convert to AttributesUpdatedMsg proto, entityData [{}]", tenantId, entityId, entityData, e); } break; case POST_ATTRIBUTES: @@ -77,7 +78,7 @@ public class EntityDataMsgConstructor { builder.setPostAttributesMsg(postAttributesMsg); builder.setPostAttributeScope(getScopeOfDefault(data)); } catch (Exception e) { - log.warn("[{}] Can't convert to PostAttributesMsg, entityData [{}]", entityId, entityData, e); + log.warn("[{}][{}] Can't convert to PostAttributesMsg, entityData [{}]", tenantId, entityId, entityData, e); } break; case ATTRIBUTES_DELETED: @@ -90,7 +91,7 @@ public class EntityDataMsgConstructor { attributeDeleteMsg.build(); builder.setAttributeDeleteMsg(attributeDeleteMsg); } catch (Exception e) { - log.warn("[{}] Can't convert to AttributeDeleteMsg proto, entityData [{}]", entityId, entityData, e); + log.warn("[{}][{}] Can't convert to AttributeDeleteMsg proto, entityData [{}]", tenantId, entityId, entityData, e); } break; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantMsgConstructor.java new file mode 100644 index 0000000000..321a57970a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantMsgConstructor.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class TenantMsgConstructor { + + public TenantUpdateMsg constructTenantUpdateMsg(UpdateMsgType msgType, Tenant tenant) { + TenantUpdateMsg.Builder builder = TenantUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(tenant.getId().getId().getMostSignificantBits()) + .setIdLSB(tenant.getId().getId().getLeastSignificantBits()) + .setTitle(tenant.getTitle()) + .setProfileIdMSB(tenant.getTenantProfileId().getId().getMostSignificantBits()) + .setProfileIdLSB(tenant.getTenantProfileId().getId().getLeastSignificantBits()) + .setRegion(tenant.getRegion()); + if (tenant.getCountry() != null) { + builder.setCountry(tenant.getCountry()); + } + if (tenant.getState() != null) { + builder.setState(tenant.getState()); + } + if (tenant.getCity() != null) { + builder.setCity(tenant.getCity()); + } + if (tenant.getAddress() != null) { + builder.setAddress(tenant.getAddress()); + } + if (tenant.getAddress2() != null) { + builder.setAddress2(tenant.getAddress2()); + } + if (tenant.getZip() != null) { + builder.setZip(tenant.getZip()); + } + if (tenant.getPhone() != null) { + builder.setPhone(tenant.getPhone()); + } + if (tenant.getEmail() != null) { + builder.setEmail(tenant.getEmail()); + } + if (tenant.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(tenant.getAdditionalInfo())); + } + return builder.build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantProfileMsgConstructor.java new file mode 100644 index 0000000000..57655c3aeb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantProfileMsgConstructor.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import com.google.protobuf.ByteString; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class TenantProfileMsgConstructor { + + @Autowired + private DataDecodingEncodingService dataDecodingEncodingService; + + public TenantProfileUpdateMsg constructTenantProfileUpdateMsg(UpdateMsgType msgType, TenantProfile tenantProfile) { + TenantProfileUpdateMsg.Builder builder = TenantProfileUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(tenantProfile.getId().getId().getMostSignificantBits()) + .setIdLSB(tenantProfile.getId().getId().getLeastSignificantBits()) + .setName(tenantProfile.getName()) + .setDefault(tenantProfile.isDefault()) + .setIsolatedRuleChain(tenantProfile.isIsolatedTbRuleEngine()) + .setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile.getProfileData()))); + if (tenantProfile.getDescription() != null) { + builder.setDescription(tenantProfile.getDescription()); + } + return builder.build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java index a574b8bb4b..e3c8d54683 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java @@ -33,11 +33,8 @@ public class WidgetTypeMsgConstructor { .setMsgType(msgType) .setIdMSB(widgetTypeDetails.getId().getId().getMostSignificantBits()) .setIdLSB(widgetTypeDetails.getId().getId().getLeastSignificantBits()); - if (widgetTypeDetails.getBundleAlias() != null) { - builder.setBundleAlias(widgetTypeDetails.getBundleAlias()); - } - if (widgetTypeDetails.getAlias() != null) { - builder.setAlias(widgetTypeDetails.getAlias()); + if (widgetTypeDetails.getFqn() != null) { + builder.setFqn(widgetTypeDetails.getFqn()); } if (widgetTypeDetails.getName() != null) { builder.setName(widgetTypeDetails.getName()); @@ -54,6 +51,7 @@ public class WidgetTypeMsgConstructor { if (widgetTypeDetails.getDescription() != null) { builder.setDescription(widgetTypeDetails.getDescription()); } + builder.setDeprecated(widgetTypeDetails.isDeprecated()); return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java index 36e43b9579..bed83c8832 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.edge.rpc.constructor; import com.google.protobuf.ByteString; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.WidgetsBundle; @@ -25,12 +26,13 @@ import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; import java.nio.charset.StandardCharsets; +import java.util.List; @Component @TbCoreComponent public class WidgetsBundleMsgConstructor { - public WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle) { + public WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle, List widgets) { WidgetsBundleUpdateMsg.Builder builder = WidgetsBundleUpdateMsg.newBuilder() .setMsgType(msgType) .setIdMSB(widgetsBundle.getId().getId().getMostSignificantBits()) @@ -46,6 +48,7 @@ public class WidgetsBundleMsgConstructor { if (widgetsBundle.getTenantId().equals(TenantId.SYS_TENANT_ID)) { builder.setIsSystem(true); } + builder.setWidgets(JacksonUtil.toString(widgets)); return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java index 28c1b1c458..0099d85bcd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java @@ -51,7 +51,7 @@ public abstract class AbstractRuleChainMetadataConstructor implements RuleChainM builder.setMsgType(msgType); return builder.build(); } catch (JsonProcessingException ex) { - log.error("Can't construct RuleChainMetadataUpdateMsg", ex); + log.error("[{}] Can't construct RuleChainMetadataUpdateMsg", tenantId, ex); } return null; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java index cefeb4c46a..b9fc1c8c9e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java @@ -85,7 +85,7 @@ public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher { result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS, EdgeEventActionType.UPDATED, null, JacksonUtil.OBJECT_MAPPER.valueToTree(tenantMailSettings))); - AdminSettings systemMailTemplates = loadMailTemplates(); + AdminSettings systemMailTemplates = loadMailTemplates(tenantId); result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS, EdgeEventActionType.UPDATED, null, JacksonUtil.OBJECT_MAPPER.valueToTree(systemMailTemplates))); @@ -97,7 +97,7 @@ public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher { return new PageData<>(result, 1, result.size(), false); } - private AdminSettings loadMailTemplates() throws Exception { + private AdminSettings loadMailTemplates(TenantId tenantId) throws Exception { Map mailTemplates = new HashMap<>(); for (String templatesName : templatesNames) { Template template = freemarkerConfig.getTemplate(templatesName); @@ -107,7 +107,7 @@ public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher { if (mailTemplate != null) { mailTemplates.put(name, mailTemplate); } else { - log.error("Can't load mail template from file {}", template.getName()); + log.error("[{}] Can't load mail template from file {}", tenantId, template.getName()); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetTypesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetTypesEdgeEventFetcher.java new file mode 100644 index 0000000000..05d9071ed3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetTypesEdgeEventFetcher.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +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.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetTypeInfo; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; + +@Slf4j +@AllArgsConstructor +public abstract class BaseWidgetTypesEdgeEventFetcher extends BasePageableEdgeEventFetcher { + + protected final WidgetTypeService widgetTypeService; + + @Override + PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + return findWidgetTypes(tenantId, pageLink); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, WidgetTypeInfo widgetTypeInfo) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.WIDGET_TYPE, + EdgeEventActionType.ADDED, widgetTypeInfo.getId(), null); + } + + protected abstract PageData findWidgetTypes(TenantId tenantId, PageLink pageLink); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DefaultProfilesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DefaultProfilesEdgeEventFetcher.java new file mode 100644 index 0000000000..1b4897dfff --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DefaultProfilesEdgeEventFetcher.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +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.dao.asset.AssetProfileService; +import org.thingsboard.server.dao.device.DeviceProfileService; + +import java.util.ArrayList; +import java.util.List; + +@AllArgsConstructor +@Slf4j +public class DefaultProfilesEdgeEventFetcher implements EdgeEventFetcher { + + private final DeviceProfileService deviceProfileService; + private final AssetProfileService assetProfileService; + + @Override + public PageLink getPageLink(int pageSize) { + return null; + } + + @Override + public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { + List result = new ArrayList<>(); + + DeviceProfile deviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId); + result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE_PROFILE, + EdgeEventActionType.ADDED, deviceProfile.getId(), null)); + + AssetProfile assetProfile = assetProfileService.findDefaultAssetProfile(tenantId); + result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET_PROFILE, + EdgeEventActionType.ADDED, assetProfile.getId(), null)); + + return new PageData<>(result, 1, result.size(), false); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..de4218fc53 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.fetch; + +import lombok.extern.slf4j.Slf4j; +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.WidgetTypeInfo; +import org.thingsboard.server.dao.widget.WidgetTypeService; + +@Slf4j +public class SystemWidgetTypesEdgeEventFetcher extends BaseWidgetTypesEdgeEventFetcher { + + public SystemWidgetTypesEdgeEventFetcher(WidgetTypeService widgetTypeService) { + super(widgetTypeService); + } + + @Override + protected PageData findWidgetTypes(TenantId tenantId, PageLink pageLink) { + return widgetTypeService.findSystemWidgetTypesByPageLink(tenantId, false, pageLink); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantEdgeEventFetcher.java new file mode 100644 index 0000000000..1e9f81be0f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantEdgeEventFetcher.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +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.dao.tenant.TenantService; + +import java.util.List; + +@AllArgsConstructor +@Slf4j +public class TenantEdgeEventFetcher extends BasePageableEdgeEventFetcher { + + private final TenantService tenantService; + + @Override + PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + Tenant tenant = tenantService.findTenantById(tenantId); + // returns PageData object to be in sync with other fetchers + return new PageData<>(List.of(tenant), 1, 1, false); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, Tenant entity) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.TENANT, + EdgeEventActionType.UPDATED, entity.getId(), null); + } +} 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 new file mode 100644 index 0000000000..24f0527c20 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.fetch; + +import lombok.extern.slf4j.Slf4j; +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.WidgetTypeInfo; +import org.thingsboard.server.dao.widget.WidgetTypeService; + +@Slf4j +public class TenantWidgetTypesEdgeEventFetcher extends BaseWidgetTypesEdgeEventFetcher { + + public TenantWidgetTypesEdgeEventFetcher(WidgetTypeService widgetTypeService) { + super(widgetTypeService); + } + @Override + protected PageData findWidgetTypes(TenantId tenantId, PageLink pageLink) { + return widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(tenantId, false, pageLink); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index ac7c791338..54aa99a5fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -21,10 +21,16 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -40,10 +46,16 @@ import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -55,18 +67,22 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.service.edge.rpc.constructor.AdminSettingsMsgConstructor; @@ -84,6 +100,8 @@ import org.thingsboard.server.service.edge.rpc.constructor.OtaPackageMsgConstruc import org.thingsboard.server.service.edge.rpc.constructor.QueueMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.RelationMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.RuleChainMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.TenantMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.TenantProfileMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.UserMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.WidgetTypeMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.WidgetsBundleMsgConstructor; @@ -104,6 +122,7 @@ import java.util.concurrent.locks.ReentrantLock; public abstract class BaseEdgeProcessor { protected static final Lock deviceCreationLock = new ReentrantLock(); + protected static final Lock assetCreationLock = new ReentrantLock(); protected static final int DEFAULT_PAGE_SIZE = 100; @@ -140,6 +159,9 @@ public abstract class BaseEdgeProcessor { @Autowired protected TenantService tenantService; + @Autowired + protected TenantProfileService tenantProfileService; + @Autowired protected EdgeService edgeService; @@ -195,6 +217,21 @@ public abstract class BaseEdgeProcessor { @Autowired protected DataValidator deviceValidator; + @Autowired + protected DataValidator deviceProfileValidator; + + @Autowired + protected DataValidator assetValidator; + + @Autowired + protected DataValidator assetProfileValidator; + + @Autowired + protected DataValidator dashboardValidator; + + @Autowired + protected DataValidator entityViewValidator; + @Autowired protected EdgeMsgConstructor edgeMsgConstructor; @@ -234,6 +271,12 @@ public abstract class BaseEdgeProcessor { @Autowired protected AssetProfileMsgConstructor assetProfileMsgConstructor; + @Autowired + protected TenantMsgConstructor tenantMsgConstructor; + + @Autowired + protected TenantProfileMsgConstructor tenantProfileMsgConstructor; + @Autowired protected WidgetsBundleMsgConstructor widgetsBundleMsgConstructor; @@ -249,15 +292,18 @@ public abstract class BaseEdgeProcessor { @Autowired protected QueueMsgConstructor queueMsgConstructor; + @Autowired + protected EdgeSynchronizationManager edgeSynchronizationManager; + @Autowired protected DbCallbackExecutorService dbCallbackExecutorService; protected ListenableFuture saveEdgeEvent(TenantId tenantId, - EdgeId edgeId, - EdgeEventType type, - EdgeEventActionType action, - EntityId entityId, - JsonNode body) { + EdgeId edgeId, + EdgeEventType type, + EdgeEventActionType action, + EntityId entityId, + JsonNode body) { log.debug("Pushing event to edge queue. tenantId [{}], edgeId [{}], type[{}], " + "action [{}], entityId [{}], body [{}]", tenantId, edgeId, type, action, entityId, body); @@ -288,7 +334,7 @@ public abstract class BaseEdgeProcessor { return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); } - protected List> processActionForAllEdgesByTenantId(TenantId tenantId, + private List> processActionForAllEdgesByTenantId(TenantId tenantId, EdgeEventType type, EdgeEventActionType actionType, EntityId entityId, @@ -340,36 +386,46 @@ public abstract class BaseEdgeProcessor { } } - protected ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + public ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); - EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, - new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); - EdgeId edgeId = safeGetEdgeId(edgeNotificationMsg); - switch (actionType) { - case ADDED: - case UPDATED: - case CREDENTIALS_UPDATED: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: - case DELETED: - if (edgeId != null) { - return saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, null); - } else { - return pushNotificationToAllRelatedEdges(tenantId, entityId, type, actionType); - } - case ASSIGNED_TO_EDGE: - case UNASSIGNED_FROM_EDGE: - ListenableFuture future = saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, null); - return Futures.transformAsync(future, unused -> { - if (type.equals(EdgeEventType.RULE_CHAIN)) { - return updateDependentRuleChains(tenantId, new RuleChainId(entityId.getId()), edgeId); + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + if (type.isAllEdgesRelated()) { + return processEntityNotificationForAllEdges(tenantId, type, actionType, entityId); + } else { + JsonNode body = JacksonUtil.toJsonNode(edgeNotificationMsg.getBody()); + EdgeId edgeId = safeGetEdgeId(edgeNotificationMsg); + switch (actionType) { + case UPDATED: + case CREDENTIALS_UPDATED: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + if (edgeId != null) { + return saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body); } else { - return Futures.immediateFuture(null); + return processNotificationToRelatedEdges(tenantId, entityId, type, actionType); } - }, dbCallbackExecutorService); - default: - return Futures.immediateFuture(null); + case DELETED: + EdgeEventActionType deleted = EdgeEventActionType.DELETED; + if (edgeId != null) { + return saveEdgeEvent(tenantId, edgeId, type, deleted, entityId, body); + } else { + return Futures.transform(Futures.allAsList(processActionForAllEdgesByTenantId(tenantId, type, deleted, entityId, body)), + voids -> null, dbCallbackExecutorService); + } + case ASSIGNED_TO_EDGE: + case UNASSIGNED_FROM_EDGE: + ListenableFuture future = saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body); + return Futures.transformAsync(future, unused -> { + if (type.equals(EdgeEventType.RULE_CHAIN)) { + return updateDependentRuleChains(tenantId, new RuleChainId(entityId.getId()), edgeId); + } else { + return Futures.immediateFuture(null); + } + }, dbCallbackExecutorService); + default: + return Futures.immediateFuture(null); + } } } @@ -381,7 +437,7 @@ public abstract class BaseEdgeProcessor { } } - private ListenableFuture pushNotificationToAllRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type, EdgeEventActionType actionType) { + private ListenableFuture processNotificationToRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type, EdgeEventActionType actionType) { PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); PageData pageData; List> futures = new ArrayList<>(); @@ -432,10 +488,7 @@ public abstract class BaseEdgeProcessor { return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); } - protected ListenableFuture processEntityNotificationForAllEdges(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); - EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); - EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + private ListenableFuture processEntityNotificationForAllEdges(TenantId tenantId, EdgeEventType type, EdgeEventActionType actionType, EntityId entityId) { switch (actionType) { case ADDED: case UPDATED: @@ -508,4 +561,39 @@ public abstract class BaseEdgeProcessor { return false; } } + + protected void createRelationFromEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { + EntityRelation relation = new EntityRelation(); + relation.setFrom(edgeId); + relation.setTo(entityId); + relation.setTypeGroup(RelationTypeGroup.COMMON); + relation.setType(EntityRelation.EDGE_TYPE); + relationService.saveRelation(tenantId, relation); + } + + protected TbMsgMetaData getEdgeActionTbMsgMetaData(Edge edge, CustomerId customerId) { + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("edgeId", edge.getId().toString()); + metaData.putValue("edgeName", edge.getName()); + if (customerId != null && !customerId.isNullUid()) { + metaData.putValue("customerId", customerId.toString()); + } + return metaData; + } + + protected void pushEntityEventToRuleEngine(TenantId tenantId, EntityId entityId, CustomerId customerId, + TbMsgType msgType, String msgData, TbMsgMetaData metaData) { + TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, msgData); + tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("[{}] Successfully send ENTITY_CREATED EVENT to rule engine [{}]", tenantId, msgData); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Failed to send ENTITY_CREATED EVENT to rule engine [{}]", tenantId, msgData, t); + } + }); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java index 189d69e68d..0b86352c02 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.edge.rpc.processor.alarm; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -62,9 +64,9 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor { AlarmId alarmId = new AlarmId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); switch (actionType) { case DELETED: - EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB())); Alarm deletedAlarm = JacksonUtil.OBJECT_MAPPER.readValue(edgeNotificationMsg.getBody(), Alarm.class); - return saveEdgeEvent(tenantId, edgeId, EdgeEventType.ALARM, actionType, alarmId, JacksonUtil.OBJECT_MAPPER.valueToTree(deletedAlarm)); + List> delFutures = pushEventToAllRelatedEdges(tenantId, deletedAlarm.getOriginator(), alarmId, actionType, JacksonUtil.OBJECT_MAPPER.valueToTree(deletedAlarm)); + return Futures.transform(Futures.allAsList(delFutures), voids -> null, dbCallbackExecutorService); default: ListenableFuture alarmFuture = alarmService.findAlarmByIdAsync(tenantId, alarmId); return Futures.transformAsync(alarmFuture, alarm -> { @@ -75,28 +77,33 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor { if (type == null) { return Futures.immediateFuture(null); } - PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - PageData pageData; - List> futures = new ArrayList<>(); - do { - pageData = edgeService.findRelatedEdgeIdsByEntityId(tenantId, alarm.getOriginator(), pageLink); - if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { - for (EdgeId relatedEdgeId : pageData.getData()) { - futures.add(saveEdgeEvent(tenantId, - relatedEdgeId, - EdgeEventType.ALARM, - EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()), - alarmId, - null)); - } - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } - } while (pageData != null && pageData.hasNext()); + List> futures = pushEventToAllRelatedEdges(tenantId, alarm.getOriginator(), alarmId, actionType, null); return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); }, dbCallbackExecutorService); } } + private List> pushEventToAllRelatedEdges(TenantId tenantId, EntityId originatorId, AlarmId alarmId, EdgeEventActionType actionType, JsonNode body) { + PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); + PageData pageData; + List> futures = new ArrayList<>(); + do { + pageData = edgeService.findRelatedEdgeIdsByEntityId(tenantId, originatorId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + for (EdgeId relatedEdgeId : pageData.getData()) { + futures.add(saveEdgeEvent(tenantId, + relatedEdgeId, + EdgeEventType.ALARM, + actionType, + alarmId, + body)); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + return futures; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java index be64f475a2..de072a48e9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java @@ -20,15 +20,21 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -45,10 +51,11 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { EntityType.valueOf(alarmUpdateMsg.getOriginatorType())); AlarmId alarmId = new AlarmId(new UUID(alarmUpdateMsg.getIdMSB(), alarmUpdateMsg.getIdLSB())); if (originatorId == null) { - log.warn("Originator not found for the alarm msg {}", alarmUpdateMsg); + log.warn("[{}] Originator not found for the alarm msg {}", tenantId, alarmUpdateMsg); return Futures.immediateFuture(null); } try { + edgeSynchronizationManager.getSync().set(true); switch (alarmUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: @@ -72,26 +79,26 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { } else { alarmService.updateAlarm(AlarmUpdateRequest.fromAlarm(alarm)); } - return Futures.immediateFuture(null); + break; case ALARM_ACK_RPC_MESSAGE: Alarm alarmToAck = alarmService.findAlarmById(tenantId, alarmId); if (alarmToAck != null) { alarmService.acknowledgeAlarm(tenantId, alarmId, alarmUpdateMsg.getAckTs()); } - return Futures.immediateFuture(null); + break; case ALARM_CLEAR_RPC_MESSAGE: Alarm alarmToClear = alarmService.findAlarmById(tenantId, alarmId); if (alarmToClear != null) { alarmService.clearAlarm(tenantId, alarmId, alarmUpdateMsg.getClearTs(), JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails())); } - return Futures.immediateFuture(null); + break; case ENTITY_DELETED_RPC_MESSAGE: Alarm alarmToDelete = alarmService.findAlarmById(tenantId, alarmId); if (alarmToDelete != null) { alarmService.delAlarm(tenantId, alarmId); } - return Futures.immediateFuture(null); + break; case UNRECOGNIZED: default: return handleUnsupportedMsgType(alarmUpdateMsg.getMsgType()); @@ -99,7 +106,10 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { } catch (Exception e) { log.error("[{}] Failed to process alarm update msg [{}]", tenantId, alarmUpdateMsg, e); return Futures.immediateFailedFuture(e); + } finally { + edgeSynchronizationManager.getSync().remove(); } + return Futures.immediateFuture(null); } private EntityId getAlarmOriginator(TenantId tenantId, String entityName, EntityType entityType) { @@ -125,13 +135,38 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { case ALARM_CLEAR: Alarm alarm = alarmService.findAlarmById(tenantId, alarmId); if (alarm != null) { - return alarmMsgConstructor.constructAlarmUpdatedMsg(tenantId, msgType, alarm); + return alarmMsgConstructor.constructAlarmUpdatedMsg(msgType, alarm, findOriginatorEntityName(tenantId, alarm)); } break; case DELETED: Alarm deletedAlarm = JacksonUtil.OBJECT_MAPPER.convertValue(body, Alarm.class); - return alarmMsgConstructor.constructAlarmUpdatedMsg(tenantId, msgType, deletedAlarm); + return alarmMsgConstructor.constructAlarmUpdatedMsg(msgType, deletedAlarm, findOriginatorEntityName(tenantId, deletedAlarm)); } return null; } + + private String findOriginatorEntityName(TenantId tenantId, Alarm alarm) { + String entityName = null; + switch (alarm.getOriginator().getEntityType()) { + case DEVICE: + Device deviceById = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId())); + if (deviceById != null) { + entityName = deviceById.getName(); + } + break; + case ASSET: + Asset assetById = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId())); + if (assetById != null) { + entityName = assetById.getName(); + } + break; + case ENTITY_VIEW: + EntityView entityViewById = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId())); + if (entityViewById != null) { + entityName = entityViewById.getName(); + } + break; + } + return entityName; + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index 2fb7320219..aa0679a632 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -15,26 +15,96 @@ */ package org.thingsboard.server.service.edge.rpc.processor.asset; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.asset.BaseAssetService; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; @Component @Slf4j @TbCoreComponent -public class AssetEdgeProcessor extends BaseEdgeProcessor { +public class AssetEdgeProcessor extends BaseAssetProcessor { + + public ListenableFuture processAssetMsgFromEdge(TenantId tenantId, Edge edge, AssetUpdateMsg assetUpdateMsg) { + log.trace("[{}] executing processAssetMsgFromEdge [{}] from edge [{}]", tenantId, assetUpdateMsg, edge.getName()); + AssetId assetId = new AssetId(new UUID(assetUpdateMsg.getIdMSB(), assetUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (assetUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + Asset assetToDelete = assetService.findAssetById(tenantId, assetId); + if (assetToDelete != null) { + assetService.unassignAssetFromEdge(tenantId, assetId, edge.getId()); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(assetUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed asset violated {}", tenantId, assetUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + private void saveOrUpdateAsset(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg, Edge edge) { + CustomerId customerId = safeGetCustomerId(assetUpdateMsg.getCustomerIdMSB(), assetUpdateMsg.getCustomerIdLSB()); + Pair resultPair = super.saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, customerId); + Boolean created = resultPair.getFirst(); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), assetId); + pushAssetCreatedEventToRuleEngine(tenantId, edge, assetId); + assetService.assignAssetToEdge(tenantId, assetId, edge.getId()); + } + Boolean assetNameUpdated = resultPair.getSecond(); + if (assetNameUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET, EdgeEventActionType.UPDATED, assetId, null); + } + } + + private void pushAssetCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AssetId assetId) { + try { + Asset asset = assetService.findAssetById(tenantId, assetId); + String assetAsString = JacksonUtil.toString(asset); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, asset.getCustomerId()); + pushEntityEventToRuleEngine(tenantId, assetId, asset.getCustomerId(), TbMsgType.ENTITY_CREATED, assetAsString, msgMetaData); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push asset action to rule engine: {}", tenantId, assetId, TbMsgType.ENTITY_CREATED.name(), e); + } + } public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent) { AssetId assetId = new AssetId(edgeEvent.getEntityId()); @@ -46,7 +116,7 @@ public class AssetEdgeProcessor extends BaseEdgeProcessor { case ASSIGNED_TO_CUSTOMER: case UNASSIGNED_FROM_CUSTOMER: Asset asset = assetService.findAssetById(edgeEvent.getTenantId(), assetId); - if (asset != null) { + if (asset != null && !BaseAssetService.TB_SERVICE_QUEUE.equals(asset.getType())) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); AssetUpdateMsg assetUpdateMsg = assetMsgConstructor.constructAssetUpdatedMsg(msgType, asset); @@ -72,8 +142,4 @@ public class AssetEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } - - public ListenableFuture processAssetNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotification(tenantId, edgeNotificationMsg); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java index 51c42e5764..3fa69ed078 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java @@ -15,25 +15,84 @@ */ package org.thingsboard.server.service.edge.rpc.processor.asset; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; @Component @Slf4j @TbCoreComponent -public class AssetProfileEdgeProcessor extends BaseEdgeProcessor { +public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor { + + public ListenableFuture processAssetProfileMsgFromEdge(TenantId tenantId, Edge edge, AssetProfileUpdateMsg assetProfileUpdateMsg) { + log.trace("[{}] executing processAssetProfileMsgFromEdge [{}] from edge [{}]", tenantId, assetProfileUpdateMsg, edge.getName()); + AssetProfileId assetProfileId = new AssetProfileId(new UUID(assetProfileUpdateMsg.getIdMSB(), assetProfileUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (assetProfileUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateAssetProfile(tenantId, assetProfileId, assetProfileUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(assetProfileUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + log.warn("[{}] Failed to process AssetProfileUpdateMsg from Edge [{}]", tenantId, assetProfileUpdateMsg, e); + return Futures.immediateFailedFuture(e); + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + private void saveOrUpdateAssetProfile(TenantId tenantId, AssetProfileId assetProfileId, AssetProfileUpdateMsg assetProfileUpdateMsg, Edge edge) { + Pair resultPair = super.saveOrUpdateAssetProfile(tenantId, assetProfileId, assetProfileUpdateMsg); + Boolean created = resultPair.getFirst(); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), assetProfileId); + pushAssetProfileCreatedEventToRuleEngine(tenantId, edge, assetProfileId); + } + Boolean assetProfileNameUpdated = resultPair.getSecond(); + if (assetProfileNameUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET_PROFILE, EdgeEventActionType.UPDATED, assetProfileId, null); + } + } + + private void pushAssetProfileCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AssetProfileId assetProfileId) { + try { + AssetProfile assetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfileId); + String assetProfileAsString = JacksonUtil.toString(assetProfile); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null); + pushEntityEventToRuleEngine(tenantId, assetProfileId, null, TbMsgType.ENTITY_CREATED, assetProfileAsString, msgMetaData); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push asset profile action to rule engine: {}", tenantId, assetProfileId, TbMsgType.ENTITY_CREATED.name(), e); + } + } public DownlinkMsg convertAssetProfileEventToDownlink(EdgeEvent edgeEvent) { AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId()); @@ -64,8 +123,20 @@ public class AssetProfileEdgeProcessor extends BaseEdgeProcessor { return downlinkMsg; } - public ListenableFuture processAssetProfileNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); + @Override + protected void setDefaultRuleChainId(TenantId tenantId, AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg) { + // do nothing on cloud } + @Override + protected void setDefaultEdgeRuleChainId(TenantId tenantId,AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg) { + UUID defaultEdgeRuleChainUUID = safeGetUUID(assetProfileUpdateMsg.getDefaultRuleChainIdMSB(), assetProfileUpdateMsg.getDefaultRuleChainIdLSB()); + assetProfile.setDefaultEdgeRuleChainId(defaultEdgeRuleChainUUID != null ? new RuleChainId(defaultEdgeRuleChainUUID) : null); + } + + @Override + protected void setDefaultDashboardId(TenantId tenantId, AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg) { + UUID defaultDashboardUUID = safeGetUUID(assetProfileUpdateMsg.getDefaultDashboardIdMSB(), assetProfileUpdateMsg.getDefaultDashboardIdLSB()); + assetProfile.setDefaultDashboardId(defaultDashboardUUID != null ? new DashboardId(defaultDashboardUUID) : null); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java new file mode 100644 index 0000000000..f8dcdbd8f6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.asset; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; + +@Slf4j +public abstract class BaseAssetProcessor extends BaseEdgeProcessor { + + protected Pair saveOrUpdateAsset(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg, CustomerId customerId) { + boolean created = false; + boolean assetNameUpdated = false; + assetCreationLock.lock(); + try { + Asset asset = assetService.findAssetById(tenantId, assetId); + String assetName = assetUpdateMsg.getName(); + if (asset == null) { + created = true; + asset = new Asset(); + asset.setTenantId(tenantId); + asset.setCreatedTime(Uuids.unixTimestamp(assetId.getId())); + } + Asset assetByName = assetService.findAssetByTenantIdAndName(tenantId, assetName); + if (assetByName != null && !assetByName.getId().equals(assetId)) { + assetName = assetName + "_" + StringUtils.randomAlphanumeric(15); + log.warn("[{}] Asset with name {} already exists. Renaming asset name to {}", + tenantId, assetUpdateMsg.getName(), assetName); + assetNameUpdated = true; + } + asset.setName(assetName); + asset.setType(assetUpdateMsg.getType()); + asset.setLabel(assetUpdateMsg.hasLabel() ? assetUpdateMsg.getLabel() : null); + asset.setAdditionalInfo(assetUpdateMsg.hasAdditionalInfo() + ? JacksonUtil.toJsonNode(assetUpdateMsg.getAdditionalInfo()) : null); + + UUID assetProfileUUID = safeGetUUID(assetUpdateMsg.getAssetProfileIdMSB(), assetUpdateMsg.getAssetProfileIdLSB()); + asset.setAssetProfileId(assetProfileUUID != null ? new AssetProfileId(assetProfileUUID) : null); + + asset.setCustomerId(customerId); + + assetValidator.validate(asset, Asset::getTenantId); + if (created) { + asset.setId(assetId); + } + assetService.saveAsset(asset, false); + } catch (Exception e) { + log.error("[{}] Failed to process asset update msg [{}]", tenantId, assetUpdateMsg, e); + throw e; + } finally { + assetCreationLock.unlock(); + } + return Pair.of(created, assetNameUpdated); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProfileProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProfileProcessor.java new file mode 100644 index 0000000000..67c8f8de44 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProfileProcessor.java @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.asset; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.nio.charset.StandardCharsets; + +@Slf4j +public abstract class BaseAssetProfileProcessor extends BaseEdgeProcessor { + + protected Pair saveOrUpdateAssetProfile(TenantId tenantId, AssetProfileId assetProfileId, AssetProfileUpdateMsg assetProfileUpdateMsg) { + boolean created = false; + boolean assetProfileNameUpdated = false; + assetCreationLock.lock(); + try { + AssetProfile assetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfileId); + String assetProfileName = assetProfileUpdateMsg.getName(); + if (assetProfile == null) { + created = true; + assetProfile = new AssetProfile(); + assetProfile.setTenantId(tenantId); + assetProfile.setCreatedTime(Uuids.unixTimestamp(assetProfileId.getId())); + } + AssetProfile assetProfileByName = assetProfileService.findAssetProfileByName(tenantId, assetProfileName); + if (assetProfileByName != null && !assetProfileByName.getId().equals(assetProfileId)) { + assetProfileName = assetProfileName + "_" + StringUtils.randomAlphabetic(15); + log.warn("[{}] Asset profile with name {} already exists. Renaming asset profile name to {}", + tenantId, assetProfileUpdateMsg.getName(), assetProfileName); + assetProfileNameUpdated = true; + } + assetProfile.setName(assetProfileName); + assetProfile.setDefault(assetProfileUpdateMsg.getDefault()); + assetProfile.setDefaultQueueName(assetProfileUpdateMsg.hasDefaultQueueName() ? assetProfileUpdateMsg.getDefaultQueueName() : null); + assetProfile.setDescription(assetProfileUpdateMsg.hasDescription() ? assetProfileUpdateMsg.getDescription() : null); + assetProfile.setImage(assetProfileUpdateMsg.hasImage() + ? new String(assetProfileUpdateMsg.getImage().toByteArray(), StandardCharsets.UTF_8) : null); + + setDefaultRuleChainId(tenantId, assetProfile, assetProfileUpdateMsg); + setDefaultEdgeRuleChainId(tenantId, assetProfile, assetProfileUpdateMsg); + setDefaultDashboardId(tenantId, assetProfile, assetProfileUpdateMsg); + + assetProfileValidator.validate(assetProfile, AssetProfile::getTenantId); + if (created) { + assetProfile.setId(assetProfileId); + } + assetProfileService.saveAssetProfile(assetProfile, false); + } catch (Exception e) { + log.error("[{}] Failed to process asset profile update msg [{}]", tenantId, assetProfileUpdateMsg, e); + throw e; + } finally { + assetCreationLock.unlock(); + } + return Pair.of(created, assetProfileNameUpdated); + } + + protected abstract void setDefaultRuleChainId(TenantId tenantId, AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg); + + protected abstract void setDefaultEdgeRuleChainId(TenantId tenantId, AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg); + + protected abstract void setDefaultDashboardId(TenantId tenantId, AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java new file mode 100644 index 0000000000..687e14a07d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java @@ -0,0 +1,84 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.dashboard; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.ShortCustomerInfo; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.Set; + +@Slf4j +public abstract class BaseDashboardProcessor extends BaseEdgeProcessor { + + protected boolean saveOrUpdateDashboard(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg, CustomerId customerId) { + boolean created = false; + Dashboard dashboard = dashboardService.findDashboardById(tenantId, dashboardId); + if (dashboard == null) { + created = true; + dashboard = new Dashboard(); + dashboard.setTenantId(tenantId); + dashboard.setCreatedTime(Uuids.unixTimestamp(dashboardId.getId())); + } + dashboard.setTitle(dashboardUpdateMsg.getTitle()); + dashboard.setImage(dashboardUpdateMsg.hasImage() ? dashboardUpdateMsg.getImage() : null); + dashboard.setConfiguration(JacksonUtil.toJsonNode(dashboardUpdateMsg.getConfiguration())); + + Set assignedCustomers = null; + if (dashboardUpdateMsg.hasAssignedCustomers()) { + assignedCustomers = JacksonUtil.fromString(dashboardUpdateMsg.getAssignedCustomers(), new TypeReference<>() {}); + assignedCustomers = filterNonExistingCustomers(tenantId, assignedCustomers); + dashboard.setAssignedCustomers(assignedCustomers); + } + + dashboard.setMobileOrder(dashboardUpdateMsg.hasMobileOrder() ? dashboardUpdateMsg.getMobileOrder() : null); + dashboard.setMobileHide(dashboardUpdateMsg.getMobileHide()); + + dashboardValidator.validate(dashboard, Dashboard::getTenantId); + if (created) { + dashboard.setId(dashboardId); + } + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard, false); + if (assignedCustomers != null && !assignedCustomers.isEmpty()) { + for (ShortCustomerInfo assignedCustomer : assignedCustomers) { + if (assignedCustomer.getCustomerId().equals(customerId)) { + dashboardService.assignDashboardToCustomer(tenantId, dashboardId, assignedCustomer.getCustomerId()); + } + } + } else { + unassignCustomersFromDashboard(tenantId, savedDashboard); + } + return created; + } + + protected abstract Set filterNonExistingCustomers(TenantId tenantId, Set assignedCustomers); + + private void unassignCustomersFromDashboard(TenantId tenantId, Dashboard dashboard) { + if (dashboard.getAssignedCustomers() != null && !dashboard.getAssignedCustomers().isEmpty()) { + for (ShortCustomerInfo assignedCustomer : dashboard.getAssignedCustomers()) { + dashboardService.unassignDashboardFromCustomer(tenantId, dashboard.getId(), assignedCustomer.getCustomerId()); + } + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index 9651609bd3..fc19d81789 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -15,25 +15,88 @@ */ package org.thingsboard.server.service.edge.rpc.processor.dashboard; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.ShortCustomerInfo; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.Set; +import java.util.UUID; @Component @Slf4j @TbCoreComponent -public class DashboardEdgeProcessor extends BaseEdgeProcessor { +public class DashboardEdgeProcessor extends BaseDashboardProcessor { + + public ListenableFuture processDashboardMsgFromEdge(TenantId tenantId, Edge edge, DashboardUpdateMsg dashboardUpdateMsg) { + log.trace("[{}] executing processDashboardMsgFromEdge [{}] from edge [{}]", tenantId, dashboardUpdateMsg, edge.getName()); + DashboardId dashboardId = new DashboardId(new UUID(dashboardUpdateMsg.getIdMSB(), dashboardUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (dashboardUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + Dashboard dashboardToDelete = dashboardService.findDashboardById(tenantId, dashboardId); + if (dashboardToDelete != null) { + dashboardService.unassignDashboardFromEdge(tenantId, dashboardId, edge.getId()); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(dashboardUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed dashboard violated {}", tenantId, dashboardUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + private void saveOrUpdateDashboard(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg, Edge edge) { + CustomerId customerId = safeGetCustomerId(dashboardUpdateMsg.getCustomerIdMSB(), dashboardUpdateMsg.getCustomerIdLSB()); + boolean created = super.saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, customerId); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), dashboardId); + pushDashboardCreatedEventToRuleEngine(tenantId, edge, dashboardId); + dashboardService.assignDashboardToEdge(tenantId, dashboardId, edge.getId()); + } + } + + private void pushDashboardCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DashboardId dashboardId) { + try { + Dashboard dashboard = dashboardService.findDashboardById(tenantId, dashboardId); + String dashboardAsString = JacksonUtil.toString(dashboard); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null); + pushEntityEventToRuleEngine(tenantId, dashboardId, null, TbMsgType.ENTITY_CREATED, dashboardAsString, msgMetaData); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push dashboard action to rule engine: {}", tenantId, dashboardId, TbMsgType.ENTITY_CREATED.name(), e); + } + } public DownlinkMsg convertDashboardEventToDownlink(EdgeEvent edgeEvent) { DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); @@ -68,7 +131,9 @@ public class DashboardEdgeProcessor extends BaseEdgeProcessor { return downlinkMsg; } - public ListenableFuture processDashboardNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotification(tenantId, edgeNotificationMsg); + @Override + protected Set filterNonExistingCustomers(TenantId tenantId, Set assignedCustomers) { + // do nothing on cloud + return assignedCustomers; } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java index 742acdbcda..7993eb4fd8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java @@ -57,13 +57,13 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { device = new Device(); device.setTenantId(tenantId); device.setCreatedTime(Uuids.unixTimestamp(deviceId.getId())); - Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName); - if (deviceByName != null) { - deviceName = deviceName + "_" + StringUtils.randomAlphabetic(15); - log.warn("Device with name {} already exists. Renaming device name to {}", - deviceUpdateMsg.getName(), deviceName); - deviceNameUpdated = true; - } + } + Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName); + if (deviceByName != null && !deviceByName.getId().equals(deviceId)) { + deviceName = deviceName + "_" + StringUtils.randomAlphabetic(15); + log.warn("[{}] Device with name {} already exists. Renaming device name to {}", + tenantId, deviceUpdateMsg.getName(), deviceName); + deviceNameUpdated = true; } device.setName(deviceName); device.setType(deviceUpdateMsg.getType()); @@ -97,8 +97,11 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { deviceCredentials.setCredentialsId(StringUtils.randomAlphanumeric(20)); deviceCredentialsService.createDeviceCredentials(device.getTenantId(), deviceCredentials); } - tbClusterService.onDeviceUpdated(savedDevice, created ? null : device, false); - } finally { + tbClusterService.onDeviceUpdated(savedDevice, created ? null : device); + } catch (Exception e) { + log.error("[{}] Failed to process device update msg [{}]", tenantId, deviceUpdateMsg, e); + throw e; + } finally { deviceCreationLock.unlock(); } return Pair.of(created, deviceNameUpdated); @@ -110,9 +113,11 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { return dbCallbackExecutorService.submit(() -> { Device device = deviceService.findDeviceById(tenantId, deviceId); if (device != null) { - log.debug("Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]", - device.getName(), deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsValue()); + log.debug("[{}] Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]", + tenantId, device.getName(), deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsValue()); try { + edgeSynchronizationManager.getSync().set(true); + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); deviceCredentials.setCredentialsType(DeviceCredentialsType.valueOf(deviceCredentialsUpdateMsg.getCredentialsType())); deviceCredentials.setCredentialsId(deviceCredentialsUpdateMsg.getCredentialsId()); @@ -120,12 +125,14 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { ? deviceCredentialsUpdateMsg.getCredentialsValue() : null); deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); } catch (Exception e) { - log.error("Can't update device credentials for device [{}], deviceCredentialsUpdateMsg [{}]", - device.getName(), deviceCredentialsUpdateMsg, e); + log.error("[{}] Can't update device credentials for device [{}], deviceCredentialsUpdateMsg [{}]", + tenantId, device.getName(), deviceCredentialsUpdateMsg, e); throw new RuntimeException(e); + } finally { + edgeSynchronizationManager.getSync().remove(); } } else { - log.warn("Can't find device by id [{}], deviceCredentialsUpdateMsg [{}]", deviceId, deviceCredentialsUpdateMsg); + log.warn("[{}] Can't find device by id [{}], deviceCredentialsUpdateMsg [{}]", tenantId, deviceId, deviceCredentialsUpdateMsg); } return null; }); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProfileProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProfileProcessor.java new file mode 100644 index 0000000000..bd35a42e59 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProfileProcessor.java @@ -0,0 +1,115 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.device; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.util.Pair; +import org.thingsboard.server.common.data.DeviceProfile; +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.StringUtils; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.UUID; + +@Slf4j +public abstract class BaseDeviceProfileProcessor extends BaseEdgeProcessor { + + @Autowired + private DataDecodingEncodingService dataDecodingEncodingService; + + protected Pair saveOrUpdateDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { + boolean created = false; + boolean deviceProfileNameUpdated = false; + deviceCreationLock.lock(); + try { + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId); + String deviceProfileName = deviceProfileUpdateMsg.getName(); + if (deviceProfile == null) { + created = true; + deviceProfile = new DeviceProfile(); + deviceProfile.setTenantId(tenantId); + deviceProfile.setCreatedTime(Uuids.unixTimestamp(deviceProfileId.getId())); + } + DeviceProfile deviceProfileByName = deviceProfileService.findDeviceProfileByName(tenantId, deviceProfileName); + if (deviceProfileByName != null && !deviceProfileByName.getId().equals(deviceProfileId)) { + deviceProfileName = deviceProfileName + "_" + StringUtils.randomAlphabetic(15); + log.warn("[{}] Device profile with name {} already exists. Renaming device profile name to {}", + tenantId, deviceProfileUpdateMsg.getName(), deviceProfileName); + deviceProfileNameUpdated = true; + } + deviceProfile.setName(deviceProfileName); + deviceProfile.setDescription(deviceProfileUpdateMsg.hasDescription() ? deviceProfileUpdateMsg.getDescription() : null); + deviceProfile.setType(DeviceProfileType.valueOf(deviceProfileUpdateMsg.getType())); + deviceProfile.setTransportType(deviceProfileUpdateMsg.hasTransportType() + ? DeviceTransportType.valueOf(deviceProfileUpdateMsg.getTransportType()) : DeviceTransportType.DEFAULT); + deviceProfile.setImage(deviceProfileUpdateMsg.hasImage() + ? new String(deviceProfileUpdateMsg.getImage().toByteArray(), StandardCharsets.UTF_8) : null); + deviceProfile.setProvisionType(deviceProfileUpdateMsg.hasProvisionType() + ? DeviceProfileProvisionType.valueOf(deviceProfileUpdateMsg.getProvisionType()) : DeviceProfileProvisionType.DISABLED); + deviceProfile.setProvisionDeviceKey(deviceProfileUpdateMsg.hasProvisionDeviceKey() + ? deviceProfileUpdateMsg.getProvisionDeviceKey() : null); + deviceProfile.setDefaultQueueName(deviceProfileUpdateMsg.getDefaultQueueName()); + + Optional profileDataOpt = + dataDecodingEncodingService.decode(deviceProfileUpdateMsg.getProfileDataBytes().toByteArray()); + deviceProfile.setProfileData(profileDataOpt.orElse(null)); + + setDefaultRuleChainId(tenantId, deviceProfile, deviceProfileUpdateMsg); + setDefaultEdgeRuleChainId(tenantId, deviceProfile, deviceProfileUpdateMsg); + setDefaultDashboardId(tenantId, deviceProfile, deviceProfileUpdateMsg); + + String defaultQueueName = StringUtils.isNotBlank(deviceProfileUpdateMsg.getDefaultQueueName()) + ? deviceProfileUpdateMsg.getDefaultQueueName() : null; + deviceProfile.setDefaultQueueName(defaultQueueName); + + UUID firmwareUUID = safeGetUUID(deviceProfileUpdateMsg.getFirmwareIdMSB(), deviceProfileUpdateMsg.getFirmwareIdLSB()); + deviceProfile.setFirmwareId(firmwareUUID != null ? new OtaPackageId(firmwareUUID) : null); + + UUID softwareUUID = safeGetUUID(deviceProfileUpdateMsg.getSoftwareIdMSB(), deviceProfileUpdateMsg.getSoftwareIdLSB()); + deviceProfile.setSoftwareId(softwareUUID != null ? new OtaPackageId(softwareUUID) : null); + + deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId); + if (created) { + deviceProfile.setId(deviceProfileId); + } + deviceProfileService.saveDeviceProfile(deviceProfile, false); + } catch (Exception e) { + log.error("[{}] Failed to process device profile update msg [{}]", tenantId, deviceProfileUpdateMsg, e); + throw e; + } finally { + deviceCreationLock.unlock(); + } + return Pair.of(created, deviceProfileNameUpdated); + } + + protected abstract void setDefaultRuleChainId(TenantId tenantId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg); + + protected abstract void setDefaultEdgeRuleChainId(TenantId tenantId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg); + + protected abstract void setDefaultDashboardId(TenantId tenantId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 3e617de8c6..15edd4215d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -35,18 +35,14 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; @@ -54,7 +50,6 @@ import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -71,6 +66,8 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { log.trace("[{}] executing processDeviceMsgFromEdge [{}] from edge [{}]", tenantId, deviceUpdateMsg, edge.getName()); DeviceId deviceId = new DeviceId(new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB())); try { + edgeSynchronizationManager.getSync().set(true); + switch (deviceUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: @@ -93,6 +90,8 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { } else { return Futures.immediateFailedFuture(e); } + } finally { + edgeSynchronizationManager.getSync().remove(); } } @@ -111,50 +110,15 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { } } - private void createRelationFromEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { - EntityRelation relation = new EntityRelation(); - relation.setFrom(edgeId); - relation.setTo(entityId); - relation.setTypeGroup(RelationTypeGroup.COMMON); - relation.setType(EntityRelation.EDGE_TYPE); - relationService.saveRelation(tenantId, relation); - } - private void pushDeviceCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DeviceId deviceId) { try { Device device = deviceService.findDeviceById(tenantId, deviceId); - ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device); - TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, device.getCustomerId(), - getActionTbMsgMetaData(edge, device.getCustomerId()), TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); - tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", device); - } - - @Override - public void onFailure(Throwable t) { - log.debug("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", device, t); - } - }); - } catch (JsonProcessingException | IllegalArgumentException e) { - log.warn("[{}] Failed to push device action to rule engine: {}", deviceId, DataConstants.ENTITY_CREATED, e); - } - } - - private TbMsgMetaData getActionTbMsgMetaData(Edge edge, CustomerId customerId) { - TbMsgMetaData metaData = getTbMsgMetaData(edge); - if (customerId != null && !customerId.isNullUid()) { - metaData.putValue("customerId", customerId.toString()); + String deviceAsString = JacksonUtil.toString(device); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, device.getCustomerId()); + pushEntityEventToRuleEngine(tenantId, deviceId, device.getCustomerId(), TbMsgType.ENTITY_CREATED, deviceAsString, msgMetaData); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push device action to rule engine: {}", tenantId, deviceId, TbMsgType.ENTITY_CREATED.name(), e); } - return metaData; - } - - private TbMsgMetaData getTbMsgMetaData(Edge edge) { - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("edgeId", edge.getId().toString()); - metaData.putValue("edgeName", edge.getName()); - return metaData; } public ListenableFuture processDeviceRpcCallFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg) { @@ -186,7 +150,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { @Override public void onFailure(Throwable t) { - log.error("Can't process push notification to core [{}]", deviceRpcCallMsg, t); + log.error("[{}] Can't process push notification to core [{}]", tenantId, deviceRpcCallMsg, t); futureToSet.setException(t); } }; @@ -216,23 +180,23 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { ObjectNode data = JacksonUtil.newObjectNode(); data.put("method", deviceRpcCallMsg.getRequestMsg().getMethod()); data.put("params", deviceRpcCallMsg.getRequestMsg().getParams()); - TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.TO_SERVER_RPC_REQUEST.name(), deviceId, null, metaData, + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.TO_SERVER_RPC_REQUEST, deviceId, null, metaData, TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(data)); tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { - log.debug("Successfully send TO_SERVER_RPC_REQUEST to rule engine [{}], deviceRpcCallMsg {}", - device, deviceRpcCallMsg); + log.debug("[{}] Successfully send TO_SERVER_RPC_REQUEST to rule engine [{}], deviceRpcCallMsg {}", + tenantId, device, deviceRpcCallMsg); } @Override public void onFailure(Throwable t) { - log.debug("Failed to send TO_SERVER_RPC_REQUEST to rule engine [{}], deviceRpcCallMsg {}", - device, deviceRpcCallMsg, t); + log.debug("[{}] Failed to send TO_SERVER_RPC_REQUEST to rule engine [{}], deviceRpcCallMsg {}", + tenantId, device, deviceRpcCallMsg, t); } }); } catch (JsonProcessingException | IllegalArgumentException e) { - log.warn("[{}] Failed to push TO_SERVER_RPC_REQUEST to rule engine. deviceRpcCallMsg {}", deviceId, deviceRpcCallMsg, e); + log.warn("[{}][{}] Failed to push TO_SERVER_RPC_REQUEST to rule engine. deviceRpcCallMsg {}", tenantId, deviceId, deviceRpcCallMsg, e); } return Futures.immediateFuture(null); @@ -308,8 +272,4 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { .addDeviceCredentialsRequestMsg(deviceCredentialsRequestMsg); return builder.build(); } - - public ListenableFuture processDeviceNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotification(tenantId, edgeNotificationMsg); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java index 5ddfecfdb1..8f1e711775 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java @@ -15,25 +15,84 @@ */ package org.thingsboard.server.service.edge.rpc.processor.device; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; @Component @Slf4j @TbCoreComponent -public class DeviceProfileEdgeProcessor extends BaseEdgeProcessor { +public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor { + + public ListenableFuture processDeviceProfileMsgFromEdge(TenantId tenantId, Edge edge, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { + log.trace("[{}] executing processDeviceProfileMsgFromEdge [{}] from edge [{}]", tenantId, deviceProfileUpdateMsg, edge.getName()); + DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(deviceProfileUpdateMsg.getIdMSB(), deviceProfileUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (deviceProfileUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateDeviceProfile(tenantId, deviceProfileId, deviceProfileUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(deviceProfileUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + log.warn("[{}] Failed to process DeviceProfileUpdateMsg from Edge [{}]", tenantId, deviceProfileUpdateMsg, e); + return Futures.immediateFailedFuture(e); + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + private void saveOrUpdateDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId, DeviceProfileUpdateMsg deviceProfileUpdateMsg, Edge edge) { + Pair resultPair = super.saveOrUpdateDeviceProfile(tenantId, deviceProfileId, deviceProfileUpdateMsg); + Boolean created = resultPair.getFirst(); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), deviceProfileId); + pushDeviceProfileCreatedEventToRuleEngine(tenantId, edge, deviceProfileId); + } + Boolean deviceProfileNameUpdated = resultPair.getSecond(); + if (deviceProfileNameUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE_PROFILE, EdgeEventActionType.UPDATED, deviceProfileId, null); + } + } + + private void pushDeviceProfileCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DeviceProfileId deviceProfileId) { + try { + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId); + String deviceProfileAsString = JacksonUtil.toString(deviceProfile); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null); + pushEntityEventToRuleEngine(tenantId, deviceProfileId, null, TbMsgType.ENTITY_CREATED, deviceProfileAsString, msgMetaData); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push device profile action to rule engine: {}", tenantId, deviceProfileId, TbMsgType.ENTITY_CREATED.name(), e); + } + } public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent) { DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); @@ -64,7 +123,20 @@ public class DeviceProfileEdgeProcessor extends BaseEdgeProcessor { return downlinkMsg; } - public ListenableFuture processDeviceProfileNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); + @Override + protected void setDefaultRuleChainId(TenantId tenantId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { + // do nothing on cloud + } + + @Override + protected void setDefaultEdgeRuleChainId(TenantId tenantId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { + UUID defaultEdgeRuleChainUUID = safeGetUUID(deviceProfileUpdateMsg.getDefaultRuleChainIdMSB(), deviceProfileUpdateMsg.getDefaultRuleChainIdLSB()); + deviceProfile.setDefaultEdgeRuleChainId(defaultEdgeRuleChainUUID != null ? new RuleChainId(defaultEdgeRuleChainUUID) : null); + } + + @Override + protected void setDefaultDashboardId(TenantId tenantId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { + UUID defaultDashboardUUID = safeGetUUID(deviceProfileUpdateMsg.getDefaultDashboardIdMSB(), deviceProfileUpdateMsg.getDefaultDashboardIdLSB()); + deviceProfile.setDefaultDashboardId(defaultDashboardUUID != null ? new DashboardId(defaultDashboardUUID) : null); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java index 123209afc1..b38659c881 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java @@ -85,7 +85,7 @@ public class EdgeProcessor extends BaseEdgeProcessor { do { pageData = userService.findCustomerUsers(tenantId, customerId, pageLink); if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { - log.trace("[{}] [{}] user(s) are going to be added to edge.", edge.getId(), pageData.getData().size()); + log.trace("[{}][{}][{}] user(s) are going to be added to edge.", tenantId, edge.getId(), pageData.getData().size()); for (User user : pageData.getData()) { futures.add(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.USER, EdgeEventActionType.ADDED, user.getId(), null)); } @@ -108,7 +108,7 @@ public class EdgeProcessor extends BaseEdgeProcessor { return Futures.immediateFuture(null); } } catch (Exception e) { - log.error("Exception during processing edge event", e); + log.error("[{}] Exception during processing edge event", tenantId, e); return Futures.immediateFailedFuture(e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java new file mode 100644 index 0000000000..cca0057707 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.entityview; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.EdgeEntityType; +import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; + +@Slf4j +public abstract class BaseEntityViewProcessor extends BaseEdgeProcessor { + + protected Pair saveOrUpdateEntityView(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg, CustomerId customerId) { + boolean created = false; + boolean entityViewNameUpdated = false; + EntityView entityView = entityViewService.findEntityViewById(tenantId, entityViewId); + String entityViewName = entityViewUpdateMsg.getName(); + if (entityView == null) { + created = true; + entityView = new EntityView(); + entityView.setTenantId(tenantId); + entityView.setCreatedTime(Uuids.unixTimestamp(entityViewId.getId())); + } + EntityView entityViewByName = entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName); + if (entityViewByName != null && !entityViewByName.getId().equals(entityViewId)) { + entityViewName = entityViewName + "_" + StringUtils.randomAlphanumeric(15); + log.warn("[{}] Entity view with name {} already exists. Renaming entity view name to {}", + tenantId, entityViewUpdateMsg.getName(), entityViewName); + entityViewNameUpdated = true; + } + entityView.setName(entityViewName); + entityView.setType(entityViewUpdateMsg.getType()); + entityView.setCustomerId(customerId); + entityView.setAdditionalInfo(entityViewUpdateMsg.hasAdditionalInfo() ? + JacksonUtil.toJsonNode(entityViewUpdateMsg.getAdditionalInfo()) : null); + + UUID entityIdUUID = safeGetUUID(entityViewUpdateMsg.getEntityIdMSB(), entityViewUpdateMsg.getEntityIdLSB()); + if (EdgeEntityType.DEVICE.equals(entityViewUpdateMsg.getEntityType())) { + entityView.setEntityId(entityIdUUID != null ? new DeviceId(entityIdUUID) : null); + } else if (EdgeEntityType.ASSET.equals(entityViewUpdateMsg.getEntityType())) { + entityView.setEntityId(entityIdUUID != null ? new AssetId(entityIdUUID) : null); + } + + entityViewValidator.validate(entityView, EntityView::getTenantId); + if (created) { + entityView.setId(entityViewId); + } + entityViewService.saveEntityView(entityView, false); + return Pair.of(created, entityViewNameUpdated); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java index 29965fcc69..01dd414aee 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -15,25 +15,94 @@ */ package org.thingsboard.server.service.edge.rpc.processor.entityview; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; @Component @Slf4j @TbCoreComponent -public class EntityViewEdgeProcessor extends BaseEdgeProcessor { +public class EntityViewEdgeProcessor extends BaseEntityViewProcessor { + + public ListenableFuture processEntityViewMsgFromEdge(TenantId tenantId, Edge edge, EntityViewUpdateMsg entityViewUpdateMsg) { + log.trace("[{}] executing processEntityViewMsgFromEdge [{}] from edge [{}]", tenantId, entityViewUpdateMsg, edge.getName()); + EntityViewId entityViewId = new EntityViewId(new UUID(entityViewUpdateMsg.getIdMSB(), entityViewUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (entityViewUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + EntityView entityViewToDelete = entityViewService.findEntityViewById(tenantId, entityViewId); + if (entityViewToDelete != null) { + entityViewService.unassignEntityViewFromEdge(tenantId, entityViewId, edge.getId()); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(entityViewUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed entity views violated {}", tenantId, entityViewUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + private void saveOrUpdateEntityView(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg, Edge edge) { + CustomerId customerId = safeGetCustomerId(entityViewUpdateMsg.getCustomerIdMSB(), entityViewUpdateMsg.getCustomerIdLSB()); + Pair resultPair = super.saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, customerId); + Boolean created = resultPair.getFirst(); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), entityViewId); + pushEntityViewCreatedEventToRuleEngine(tenantId, edge, entityViewId); + entityViewService.assignEntityViewToEdge(tenantId, entityViewId, edge.getId()); + } + Boolean assetNameUpdated = resultPair.getSecond(); + if (assetNameUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ENTITY_VIEW, EdgeEventActionType.UPDATED, entityViewId, null); + } + } + + private void pushEntityViewCreatedEventToRuleEngine(TenantId tenantId, Edge edge, EntityViewId entityViewId) { + try { + EntityView entityView = entityViewService.findEntityViewById(tenantId, entityViewId); + String entityViewAsString = JacksonUtil.toString(entityView); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, entityView.getCustomerId()); + pushEntityEventToRuleEngine(tenantId, entityViewId, entityView.getCustomerId(), TbMsgType.ENTITY_CREATED, entityViewAsString, msgMetaData); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push entity view action to rule engine: {}", tenantId, entityViewId, TbMsgType.ENTITY_CREATED.name(), e); + } + } public DownlinkMsg convertEntityViewEventToDownlink(EdgeEvent edgeEvent) { EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); @@ -67,8 +136,4 @@ public class EntityViewEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } - - public ListenableFuture processEntityViewNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotification(tenantId, edgeNotificationMsg); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java index 8206e0f1b0..fae6399e3a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java @@ -15,18 +15,15 @@ */ package org.thingsboard.server.service.edge.rpc.processor.ota; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.OtaPackageId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @@ -63,8 +60,4 @@ public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } - - public ListenableFuture processOtaPackageNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java index cbacaf9276..8562582940 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java @@ -15,18 +15,15 @@ */ package org.thingsboard.server.service.edge.rpc.processor.queue; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.QueueId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @@ -64,7 +61,4 @@ public class QueueEdgeProcessor extends BaseEdgeProcessor { return downlinkMsg; } - public ListenableFuture processQueueNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java index df683bbbe2..f7be0cf1f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java @@ -34,8 +34,9 @@ import java.util.UUID; public abstract class BaseRelationProcessor extends BaseEdgeProcessor { public ListenableFuture processRelationMsg(TenantId tenantId, RelationUpdateMsg relationUpdateMsg) { - log.trace("[{}] processRelationFromEdge [{}]", tenantId, relationUpdateMsg); + log.trace("[{}] processRelationMsg [{}]", tenantId, relationUpdateMsg); try { + edgeSynchronizationManager.getSync().set(true); EntityRelation entityRelation = new EntityRelation(); UUID fromUUID = new UUID(relationUpdateMsg.getFromIdMSB(), relationUpdateMsg.getFromIdLSB()); @@ -55,15 +56,15 @@ public abstract class BaseRelationProcessor extends BaseEdgeProcessor { case ENTITY_UPDATED_RPC_MESSAGE: if (isEntityExists(tenantId, entityRelation.getTo()) && isEntityExists(tenantId, entityRelation.getFrom())) { - return Futures.transform(relationService.saveRelationAsync(tenantId, entityRelation), - (result) -> null, dbCallbackExecutorService); + relationService.saveRelation(tenantId, entityRelation); + break; } else { - log.warn("Skipping relating update msg because from/to entity doesn't exists on edge, {}", relationUpdateMsg); - return Futures.immediateFuture(null); + log.warn("[{}] Skipping relating update msg because from/to entity doesn't exists on edge, {}", tenantId, relationUpdateMsg); + break; } case ENTITY_DELETED_RPC_MESSAGE: - return Futures.transform(relationService.deleteRelationAsync(tenantId, entityRelation), - (result) -> null, dbCallbackExecutorService); + relationService.deleteRelation(tenantId, entityRelation); + break; case UNRECOGNIZED: default: return handleUnsupportedMsgType(relationUpdateMsg.getMsgType()); @@ -71,6 +72,9 @@ public abstract class BaseRelationProcessor extends BaseEdgeProcessor { } catch (Exception e) { log.error("[{}] Failed to process relation update msg [{}]", tenantId, relationUpdateMsg, e); return Futures.immediateFailedFuture(e); + } finally { + edgeSynchronizationManager.getSync().remove(); } + return Futures.immediateFuture(null); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index 69d4f7ef64..ba6fc1ef97 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -15,13 +15,11 @@ */ package org.thingsboard.server.service.edge.rpc.processor.rule; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; @@ -29,7 +27,6 @@ import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @@ -93,8 +90,4 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } - - public ListenableFuture processRuleChainNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotification(tenantId, edgeNotificationMsg); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java index fece86b92a..c2d172ede0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java @@ -50,11 +50,11 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.dao.model.ModelConstants; @@ -130,7 +130,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { result.add(processAttributeDeleteMsg(tenantId, entityId, entityData.getAttributeDeleteMsg(), entityData.getEntityType())); } } else { - log.warn("Skipping telemetry update msg because entity doesn't exists on edge, {}", entityData); + log.warn("[{}] Skipping telemetry update msg because entity doesn't exists on edge, {}", tenantId, entityData); } return result; } @@ -172,7 +172,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { } break; default: - log.debug("Using empty metadata for entityId [{}]", entityId); + log.debug("[{}] Using empty metadata for entityId [{}]", tenantId, entityId); break; } return new ImmutablePair<>(metaData, customerId != null ? customerId : new CustomerId(ModelConstants.NULL_UUID)); @@ -184,7 +184,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { JsonObject json = JsonUtils.getJsonObject(tsKv.getKvList()); metaData.putValue("ts", tsKv.getTs() + ""); var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); - TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); + TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), TbMsgType.POST_TELEMETRY_REQUEST, entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { @@ -193,7 +193,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { @Override public void onFailure(Throwable t) { - log.error("Can't process post telemetry [{}]", msg, t); + log.error("[{}] Can't process post telemetry [{}]", tenantId, msg, t); futureToSet.setException(t); } }); @@ -207,7 +207,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { if (EntityType.DEVICE.equals(entityId.getEntityType())) { DeviceProfile deviceProfile = deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); if (deviceProfile == null) { - log.warn("[{}] Device profile is null!", entityId); + log.warn("[{}][{}] Device profile is null!", tenantId, entityId); } else { ruleChainId = deviceProfile.getDefaultRuleChainId(); queueName = deviceProfile.getDefaultQueueName(); @@ -215,7 +215,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { } else if (EntityType.ASSET.equals(entityId.getEntityType())) { AssetProfile assetProfile = assetProfileCache.get(tenantId, new AssetId(entityId.getId())); if (assetProfile == null) { - log.warn("[{}] Asset profile is null!", entityId); + log.warn("[{}][{}] Asset profile is null!", tenantId, entityId); } else { ruleChainId = assetProfile.getDefaultRuleChainId(); queueName = assetProfile.getDefaultQueueName(); @@ -228,7 +228,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { SettableFuture futureToSet = SettableFuture.create(); JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); - TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); + TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { @@ -237,7 +237,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { @Override public void onFailure(Throwable t) { - log.error("Can't process post attributes [{}]", msg, t); + log.error("[{}] Can't process post attributes [{}]", tenantId, msg, t); futureToSet.setException(t); } }); @@ -257,7 +257,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { @Override public void onSuccess(@Nullable Void tmp) { var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); - TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), DataConstants.ATTRIBUTES_UPDATED, entityId, + TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), TbMsgType.ATTRIBUTES_UPDATED, entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { @Override @@ -267,7 +267,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { @Override public void onFailure(Throwable t) { - log.error("Can't process attributes update [{}]", msg, t); + log.error("[{}] Can't process attributes update [{}]", tenantId, msg, t); futureToSet.setException(t); } }); @@ -275,7 +275,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { @Override public void onFailure(Throwable t) { - log.error("Can't process attributes update [{}]", msg, t); + log.error("[{}] Can't process attributes update [{}]", tenantId, msg, t); futureToSet.setException(t); } }); @@ -300,7 +300,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { @Override public void onFailure(Throwable t) { - log.error("Can't process attribute delete msg [{}]", attributeDeleteMsg, t); + log.error("[{}] Can't process attribute delete msg [{}]", tenantId, attributeDeleteMsg, t); futureToSet.setException(t); } }); @@ -311,7 +311,8 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { }, dbCallbackExecutorService); } - public EntityDataProto convertTelemetryEventToEntityDataProto(EntityType entityType, + public EntityDataProto convertTelemetryEventToEntityDataProto(TenantId tenantId, + EntityType entityType, UUID entityUUID, EdgeEventActionType actionType, JsonNode body) throws JsonProcessingException { @@ -342,11 +343,11 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { entityId = new EdgeId(entityUUID); break; default: - log.warn("Unsupported edge event type [{}]", entityType); + log.warn("[{}] Unsupported edge event type [{}]", tenantId, entityType); return null; } JsonElement entityData = JsonParser.parseString(JacksonUtil.OBJECT_MAPPER.writeValueAsString(body)); - return entityDataMsgConstructor.constructEntityDataMsg(entityId, actionType, entityData); + return entityDataMsgConstructor.constructEntityDataMsg(tenantId, entityId, actionType, entityData); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java index 66c5596545..861d766638 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java @@ -38,7 +38,8 @@ public class TelemetryEdgeProcessor extends BaseTelemetryProcessor { public DownlinkMsg convertTelemetryEventToDownlink(EdgeEvent edgeEvent) throws JsonProcessingException { EntityType entityType = EntityType.valueOf(edgeEvent.getType().name()); - EntityDataProto entityDataProto = convertTelemetryEventToEntityDataProto(entityType, edgeEvent.getEntityId(), + EntityDataProto entityDataProto = convertTelemetryEventToEntityDataProto( + edgeEvent.getTenantId(), entityType, edgeEvent.getEntityId(), edgeEvent.getAction(), edgeEvent.getBody()); return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java new file mode 100644 index 0000000000..9bd87660d2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.tenant; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class TenantEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertTenantEventToDownlink(EdgeEvent edgeEvent) { + TenantId tenantId = new TenantId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) { + Tenant tenant = tenantService.findTenantById(tenantId); + if (tenant != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + TenantUpdateMsg tenantUpdateMsg = tenantMsgConstructor.constructTenantUpdateMsg(msgType, tenant); + TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId()); + TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addTenantUpdateMsg(tenantUpdateMsg) + .addTenantProfileUpdateMsg(tenantProfileUpdateMsg) + .build(); + } + } + return downlinkMsg; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java new file mode 100644 index 0000000000..c9071571c4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.tenant; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Component +@Slf4j +@TbCoreComponent +public class TenantProfileEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertTenantProfileEventToDownlink(EdgeEvent edgeEvent) { + TenantProfileId tenantProfileId = new TenantProfileId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) { + TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(edgeEvent.getTenantId(), tenantProfileId); + if (tenantProfile != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + TenantProfileUpdateMsg tenantProfileUpdateMsg = + tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addTenantProfileUpdateMsg(tenantProfileUpdateMsg) + .build(); + } + } + return downlinkMsg; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index 9070686fd6..de40fdb1a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -15,19 +15,16 @@ */ package org.thingsboard.server.service.edge.rpc.processor.user; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @@ -67,11 +64,9 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { .addUserCredentialsUpdateMsg(userCredentialsUpdateMsg) .build(); } + break; } return downlinkMsg; } - public ListenableFuture processUserNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java index a429816a31..877ece1b15 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java @@ -15,21 +15,20 @@ */ package org.thingsboard.server.service.edge.rpc.processor.widget; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import java.util.List; + @Component @Slf4j @TbCoreComponent @@ -43,9 +42,10 @@ public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { case UPDATED: WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleById(edgeEvent.getTenantId(), widgetsBundleId); if (widgetsBundle != null) { + List widgets = widgetTypeService.findWidgetFqnsByWidgetsBundleId(edgeEvent.getTenantId(), widgetsBundleId); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = - widgetsBundleMsgConstructor.constructWidgetsBundleUpdateMsg(msgType, widgetsBundle); + widgetsBundleMsgConstructor.constructWidgetsBundleUpdateMsg(msgType, widgetsBundle, widgets); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addWidgetsBundleUpdateMsg(widgetsBundleUpdateMsg) @@ -63,8 +63,4 @@ public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } - - public ListenableFuture processWidgetsBundleNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java index 3caad57081..5171724439 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java @@ -15,18 +15,15 @@ */ package org.thingsboard.server.service.edge.rpc.processor.widget; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @@ -64,7 +61,4 @@ public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { return downlinkMsg; } - public ListenableFuture processWidgetTypeNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - return processEntityNotificationForAllEdges(tenantId, edgeNotificationMsg); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 700e0f3861..46bb940da9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -167,6 +167,8 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { attributes.put(attr.getKey(), attr.getDoubleValue().get()); } else if (attr.getDataType() == DataType.LONG && attr.getLongValue().isPresent()) { attributes.put(attr.getKey(), attr.getLongValue().get()); + } else if (attr.getDataType() == DataType.JSON && attr.getJsonValue().isPresent()) { + attributes.set(attr.getKey(), JacksonUtil.toJsonNode(attr.getJsonValue().get())); } else { attributes.put(attr.getKey(), attr.getValueAsString()); } @@ -175,7 +177,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { entityData.put("kv", attributes); entityData.put("scope", scope); JsonNode body = JacksonUtil.OBJECT_MAPPER.valueToTree(entityData); - log.debug("Sending attributes data msg, entityId [{}], attributes [{}]", entityId, body); + log.debug("[{}] Sending attributes data msg, entityId [{}], attributes [{}]", tenantId, entityId, body); future = saveEdgeEvent(tenantId, edge.getId(), entityType, EdgeEventActionType.ATTRIBUTES_UPDATED, entityId, body); } else { future = Futures.immediateFuture(null); @@ -183,7 +185,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { } return Futures.transformAsync(future, v -> processLatestTimeseriesAndAddToEdgeQueue(tenantId, entityId, edge, entityType), dbCallbackExecutorService); } catch (Exception e) { - String errMsg = String.format("[%s] Failed to save attribute updates to the edge [%s]", edge.getId(), attributesRequestMsg); + String errMsg = String.format("[%s][%s] Failed to save attribute updates to the edge [%s]", tenantId, edge.getId(), attributesRequestMsg); log.error(errMsg, e); return Futures.immediateFailedFuture(new RuntimeException(errMsg, e)); } @@ -237,7 +239,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { if (relationsList != null && !relationsList.isEmpty()) { List> futures = new ArrayList<>(); for (List entityRelations : relationsList) { - log.trace("[{}] [{}] [{}] relation(s) are going to be pushed to edge.", edge.getId(), entityId, entityRelations.size()); + log.trace("[{}][{}][{}][{}] relation(s) are going to be pushed to edge.", tenantId, edge.getId(), entityId, entityRelations.size()); for (EntityRelation relation : entityRelations) { try { if (!relation.getFrom().getEntityType().equals(EntityType.EDGE) && @@ -250,7 +252,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { JacksonUtil.OBJECT_MAPPER.valueToTree(relation))); } } catch (Exception e) { - String errMsg = String.format("[%s] Exception during loading relation [%s] to edge on sync!", edge.getId(), relation); + String errMsg = String.format("[%s][%s] Exception during loading relation [%s] to edge on sync!", tenantId, edge.getId(), relation); log.error(errMsg, e); futureToSet.setException(new RuntimeException(errMsg, e)); return; @@ -265,7 +267,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Override public void onFailure(Throwable throwable) { - String errMsg = String.format("[%s] Exception during saving edge events [%s]!", edge.getId(), relationRequestMsg); + String errMsg = String.format("[%s][%s] Exception during saving edge events [%s]!", tenantId, edge.getId(), relationRequestMsg); log.error(errMsg, throwable); futureToSet.setException(new RuntimeException(errMsg, throwable)); } @@ -274,7 +276,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { futureToSet.set(null); } } catch (Exception e) { - log.error("Exception during loading relation(s) to edge on sync!", e); + log.error("[{}] Exception during loading relation(s) to edge on sync!", tenantId, e); futureToSet.setException(e); } } @@ -328,7 +330,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { WidgetsBundle widgetsBundleById = widgetsBundleService.findWidgetsBundleById(tenantId, widgetsBundleId); if (widgetsBundleById != null) { List widgetTypesToPush = - widgetTypeService.findWidgetTypesByTenantIdAndBundleAlias(widgetsBundleById.getTenantId(), widgetsBundleById.getAlias()); + widgetTypeService.findWidgetTypesByWidgetsBundleId(widgetsBundleById.getTenantId(), widgetsBundleId); for (WidgetType widgetType : widgetTypesToPush) { futures.add(saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.WIDGET_TYPE, EdgeEventActionType.ADDED, widgetType.getId(), null)); } @@ -372,7 +374,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Override public void onFailure(Throwable t) { - log.error("Exception during loading relation to edge on sync!", t); + log.error("[{}] Exception during loading relation to edge on sync!", tenantId, t); futureToSet.setException(t); } }, dbCallbackExecutorService); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index 0a75404fb4..f2465c9398 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -21,29 +21,21 @@ import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmComment; -import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -51,8 +43,6 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; -import java.util.List; - @Slf4j @Service @RequiredArgsConstructor @@ -98,54 +88,6 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } } - @Override - public void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, - CustomerId customerId, ActionType actionType, - List relatedEdgeIds, - User user, Object... additionalInfo) { - logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); - sendDeleteNotificationMsg(tenantId, entityId, relatedEdgeIds, null); - } - - @Override - public void notifyDeleteAlarm(TenantId tenantId, Alarm alarm, EntityId originatorId, CustomerId customerId, - List relatedEdgeIds, User user, String body, Object... additionalInfo) { - logEntityAction(tenantId, originatorId, alarm, customerId, ActionType.DELETED, user, additionalInfo); - sendAlarmDeleteNotificationMsg(tenantId, alarm, relatedEdgeIds, body); - } - - @Override - public void notifyDeleteRuleChain(TenantId tenantId, RuleChain ruleChain, List relatedEdgeIds, User user) { - RuleChainId ruleChainId = ruleChain.getId(); - logEntityAction(tenantId, ruleChainId, ruleChain, null, ActionType.DELETED, user, null, ruleChainId.toString()); - if (RuleChainType.EDGE.equals(ruleChain.getType())) { - sendDeleteNotificationMsg(tenantId, ruleChainId, relatedEdgeIds, null); - } - } - - @Override - public void notifySendMsgToEdgeService(TenantId tenantId, I entityId, EdgeEventActionType edgeEventActionType) { - sendEntityNotificationMsg(tenantId, entityId, edgeEventActionType); - } - - @Override - public void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, - CustomerId customerId, E entity, - ActionType actionType, - User user, Object... additionalInfo) { - logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); - sendEntityNotificationMsg(tenantId, entityId, edgeTypeByActionType(actionType), JacksonUtil.toString(customerId)); - } - - @Override - public void notifyAssignOrUnassignEntityToEdge(TenantId tenantId, I entityId, - CustomerId customerId, EdgeId edgeId, - E entity, ActionType actionType, - User user, Object... additionalInfo) { - logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); - sendEntityAssignToEdgeNotificationMsg(tenantId, edgeId, entityId, edgeTypeByActionType(actionType)); - } - @Override public void notifyCreateOrUpdateTenant(Tenant tenant, ComponentLifecycleEvent event) { tbClusterService.onTenantChange(tenant, null); @@ -168,18 +110,16 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS @Override public void notifyDeleteDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, - List relatedEdgeIds, User user, Object... additionalInfo) { + User user, Object... additionalInfo) { gatewayNotificationsService.onDeviceDeleted(device); tbClusterService.onDeviceDeleted(device, null); - - notifyDeleteEntity(tenantId, deviceId, device, customerId, ActionType.DELETED, relatedEdgeIds, user, additionalInfo); + logEntityAction(tenantId, deviceId, device, customerId, ActionType.DELETED, user, additionalInfo); } @Override public void notifyUpdateDeviceCredentials(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, DeviceCredentials deviceCredentials, User user) { tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceCredentials.getDeviceId(), deviceCredentials), null); - sendEntityNotificationMsg(tenantId, deviceId, EdgeEventActionType.CREDENTIALS_UPDATED); logEntityAction(tenantId, deviceId, device, customerId, ActionType.CREDENTIALS_UPDATED, user, deviceCredentials); } @@ -190,16 +130,6 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS pushAssignedFromNotification(tenant, newTenantId, device); } - @Override - public void notifyCreateOrUpdateEntity(TenantId tenantId, I entityId, E entity, - CustomerId customerId, ActionType actionType, - User user, Object... additionalInfo) { - logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); - if (actionType == ActionType.UPDATED) { - sendEntityNotificationMsg(tenantId, entityId, EdgeEventActionType.UPDATED); - } - } - @Override public void notifyCreateOrUpdateOrDeleteEdge(TenantId tenantId, EdgeId edgeId, CustomerId customerId, Edge edge, ActionType actionType, User user, Object... additionalInfo) { @@ -222,71 +152,16 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } @Override - public void notifyCreateOrUpdateAlarm(AlarmInfo alarm, ActionType actionType, User user, Object... additionalInfo) { - logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarm, alarm.getCustomerId(), actionType, user, additionalInfo); - sendEntityNotificationMsg(alarm.getTenantId(), alarm.getId(), edgeTypeByActionType(actionType)); - } - - @Override - public void notifyAlarmComment(Alarm alarm, AlarmComment alarmComment, ActionType actionType, User user) { - logEntityAction(alarm.getTenantId(), alarm.getId(), alarm, alarm.getCustomerId(), actionType, user, alarmComment); - } - - @Override - public void notifyCreateOrUpdateOrDelete(TenantId tenantId, CustomerId customerId, - I entityId, E entity, User user, - ActionType actionType, boolean sendNotifyMsgToEdge, Exception e, - Object... additionalInfo) { - logEntityAction(tenantId, entityId, entity, customerId, actionType, user, e, additionalInfo); - if (sendNotifyMsgToEdge) { - sendEntityNotificationMsg(tenantId, entityId, edgeTypeByActionType(actionType)); - } - } - - @Override - public void notifyRelation(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user, - ActionType actionType, Object... additionalInfo) { - logEntityAction(tenantId, relation.getFrom(), null, customerId, actionType, user, additionalInfo); - logEntityAction(tenantId, relation.getTo(), null, customerId, actionType, user, additionalInfo); - if (!EntityType.EDGE.equals(relation.getFrom().getEntityType()) && !EntityType.EDGE.equals(relation.getTo().getEntityType())) { - sendNotificationMsgToEdge(tenantId, null, null, JacksonUtil.toString(relation), - EdgeEventType.RELATION, edgeTypeByActionType(actionType)); - } - } - - private void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) { - sendEntityNotificationMsg(tenantId, entityId, action, null); - } - - private void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action, String body) { - sendNotificationMsgToEdge(tenantId, null, entityId, body, null, action); - } - - private void sendAlarmDeleteNotificationMsg(TenantId tenantId, Alarm alarm, List edgeIds, String body) { - sendDeleteNotificationMsg(tenantId, alarm.getId(), edgeIds, body); - } - - private void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, String body) { - if (edgeIds != null && !edgeIds.isEmpty()) { - for (EdgeId edgeId : edgeIds) { - sendNotificationMsgToEdge(tenantId, edgeId, entityId, body, null, EdgeEventActionType.DELETED); - } - } - } - - private void sendEntityAssignToEdgeNotificationMsg(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType action) { - sendNotificationMsgToEdge(tenantId, edgeId, entityId, null, null, action); - } - - private void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, - EdgeEventType type, EdgeEventActionType action) { - tbClusterService.sendNotificationMsgToEdge(tenantId, edgeId, entityId, body, type, action); + public void logEntityRelationAction(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user, + ActionType actionType, Exception e, Object... additionalInfo) { + logEntityAction(tenantId, relation.getFrom(), null, customerId, actionType, user, e, additionalInfo); + logEntityAction(tenantId, relation.getTo(), null, customerId, actionType, user, e, additionalInfo); } private void pushAssignedFromNotification(Tenant currentTenant, TenantId newTenantId, Device assignedDevice) { String data = JacksonUtil.toString(JacksonUtil.valueToTree(assignedDevice)); if (data != null) { - TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_ASSIGNED_FROM_TENANT, assignedDevice.getId(), + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_ASSIGNED_FROM_TENANT, assignedDevice.getId(), assignedDevice.getCustomerId(), getMetaDataForAssignedFrom(currentTenant), TbMsgDataType.JSON, data); tbClusterService.pushMsgToRuleEngine(newTenantId, assignedDevice.getId(), tbMsg, null); } @@ -327,6 +202,8 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS return EdgeEventActionType.ASSIGNED_TO_EDGE; case UNASSIGNED_FROM_EDGE: return EdgeEventActionType.UNASSIGNED_FROM_EDGE; + case CREDENTIALS_UPDATED: + return EdgeEventActionType.CREDENTIALS_UPDATED; default: return null; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java index c5f8f85831..d14b2c6293 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -19,12 +19,8 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmComment; -import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; @@ -32,11 +28,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.security.DeviceCredentials; -import java.util.List; - public interface TbNotificationEntityService { void logEntityAction(TenantId tenantId, I entityId, ActionType actionType, User user, @@ -55,33 +48,6 @@ public interface TbNotificationEntityService { ActionType actionType, User user, Exception e, Object... additionalInfo); - void notifyCreateOrUpdateEntity(TenantId tenantId, I entityId, E entity, - CustomerId customerId, ActionType actionType, - User user, Object... additionalInfo); - - void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, - CustomerId customerId, ActionType actionType, - List relatedEdgeIds, - User user, Object... additionalInfo); - - void notifyDeleteAlarm(TenantId tenantId, Alarm alarm, EntityId originatorId, CustomerId customerId, - List relatedEdgeIds, User user, String body, Object... additionalInfo); - - void notifyDeleteRuleChain(TenantId tenantId, RuleChain ruleChain, - List relatedEdgeIds, User user); - - void notifySendMsgToEdgeService(TenantId tenantId, I entityId, EdgeEventActionType edgeEventActionType); - - void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, - CustomerId customerId, E entity, - ActionType actionType, - User user, Object... additionalInfo); - - void notifyAssignOrUnassignEntityToEdge(TenantId tenantId, I entityId, - CustomerId customerId, EdgeId edgeId, - E entity, ActionType actionType, - User user, Object... additionalInfo); - void notifyCreateOrUpdateTenant(Tenant tenant, ComponentLifecycleEvent event); void notifyDeleteTenant(Tenant tenant); @@ -90,7 +56,7 @@ public interface TbNotificationEntityService { Device oldDevice, ActionType actionType, User user, Object... additionalInfo); void notifyDeleteDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, - List relatedEdgeIds, User user, Object... additionalInfo); + User user, Object... additionalInfo); void notifyUpdateDeviceCredentials(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, DeviceCredentials deviceCredentials, User user); @@ -101,16 +67,6 @@ public interface TbNotificationEntityService { void notifyCreateOrUpdateOrDeleteEdge(TenantId tenantId, EdgeId edgeId, CustomerId customerId, Edge edge, ActionType actionType, User user, Object... additionalInfo); - void notifyCreateOrUpdateAlarm(AlarmInfo alarm, ActionType actionType, User user, Object... additionalInfo); - - void notifyAlarmComment(Alarm alarm, AlarmComment alarmComment, ActionType actionType, User user); - - - void notifyCreateOrUpdateOrDelete(TenantId tenantId, CustomerId customerId, - I entityId, E entity, User user, - ActionType actionType, boolean sendNotifyMsgToEdge, - Exception e, Object... additionalInfo); - - void notifyRelation(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user, - ActionType actionType, Object... additionalInfo); + void logEntityRelationAction(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user, + ActionType actionType, Exception e, Object... additionalInfo); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java index 6dfc81c747..282d11ab2a 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java @@ -45,7 +45,8 @@ public class DefaultTbAlarmCommentService extends AbstractTbEntityService implem } try { AlarmComment savedAlarmComment = checkNotNull(alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment)); - notificationEntityService.notifyAlarmComment(alarm, savedAlarmComment, actionType, user); + notificationEntityService.logEntityAction(alarm.getTenantId(), alarm.getId(), alarm, alarm.getCustomerId(), actionType, user, savedAlarmComment); + return savedAlarmComment; } catch (Exception e) { notificationEntityService.logEntityAction(alarm.getTenantId(), emptyId(EntityType.ALARM), alarm, actionType, user, e, alarmComment); @@ -62,7 +63,7 @@ public class DefaultTbAlarmCommentService extends AbstractTbEntityService implem String.format("User %s deleted his comment", (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))); AlarmComment savedAlarmComment = checkNotNull(alarmCommentService.saveAlarmComment(alarm.getTenantId(), alarmComment)); - notificationEntityService.notifyAlarmComment(alarm, savedAlarmComment, ActionType.DELETED_COMMENT, user); + notificationEntityService.logEntityAction(alarm.getTenantId(), alarm.getId(), alarm, alarm.getCustomerId(), ActionType.DELETED_COMMENT, user, savedAlarmComment); } else { throw new ThingsboardException("System comment could not be deleted", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 07c66e359a..bd09b04a02 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.entitiy.alarm; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -29,21 +30,20 @@ import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; -import org.thingsboard.server.common.data.alarm.AlarmQueryV2; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; @Service @AllArgsConstructor @@ -67,10 +67,6 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb if (!result.isSuccessful()) { throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); } - actionType = result.isCreated() ? ActionType.ADDED : ActionType.UPDATED; - if (result.isModified()) { - notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), actionType, user); - } AlarmInfo resultAlarm = result.getAlarm(); if (alarm.isAcknowledged() && !resultAlarm.isAcknowledged()) { resultAlarm = ack(resultAlarm, alarm.getAckTs(), user); @@ -85,6 +81,10 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } else if (newAssignee == null && curAssignee != null) { resultAlarm = unassign(alarm, alarm.getAssignTs(), user); } + if (result.isModified()) { + notificationEntityService.logEntityAction(tenantId, alarm.getOriginator(), resultAlarm, + resultAlarm.getCustomerId(), actionType, user); + } return new Alarm(resultAlarm); } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ALARM), alarm, actionType, user, e); @@ -103,25 +103,16 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb if (!result.isSuccessful()) { throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); } + AlarmInfo alarmInfo = result.getAlarm(); if (result.isModified()) { - AlarmComment alarmComment = AlarmComment.builder() - .alarmId(alarm.getId()) - .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was acknowledged by user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString()) - .put("subtype", "ACK")) - .build(); - try { - alarmCommentService.saveAlarmComment(alarm, alarmComment, user); - } catch (ThingsboardException e) { - log.error("Failed to save alarm comment", e); - } - notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ACK, user); + String systemComment = String.format("Alarm was acknowledged by user %s", user.getTitle()); + addSystemAlarmComment(alarmInfo, user, "ACK", systemComment); + notificationEntityService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, + alarmInfo.getCustomerId(), ActionType.ALARM_ACK, user); } else { throw new ThingsboardException("Alarm was already acknowledged!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } - return result.getAlarm(); + return alarmInfo; } @Override @@ -135,25 +126,16 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb if (!result.isSuccessful()) { throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); } + AlarmInfo alarmInfo = result.getAlarm(); if (result.isCleared()) { - AlarmComment alarmComment = AlarmComment.builder() - .alarmId(alarm.getId()) - .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was cleared by user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString()) - .put("subtype", "CLEAR")) - .build(); - try { - alarmCommentService.saveAlarmComment(alarm, alarmComment, user); - } catch (ThingsboardException e) { - log.error("Failed to save alarm comment", e); - } - notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_CLEAR, user); + String systemComment = String.format("Alarm was cleared by user %s", user.getTitle()); + addSystemAlarmComment(alarmInfo, user, "CLEAR", systemComment); + notificationEntityService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, + alarmInfo.getCustomerId(), ActionType.ALARM_CLEAR, user); } else { throw new ThingsboardException("Alarm was already cleared!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } - return result.getAlarm(); + return alarmInfo; } @Override @@ -165,22 +147,10 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb AlarmInfo alarmInfo = result.getAlarm(); if (result.isModified()) { AlarmAssignee assignee = alarmInfo.getAssignee(); - AlarmComment alarmComment = AlarmComment.builder() - .alarmId(alarm.getId()) - .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was assigned by user %s to user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName(), - (assignee.getFirstName() == null || assignee.getLastName() == null) ? assignee.getEmail() : assignee.getFirstName() + " " + assignee.getLastName())) - .put("userId", user.getId().toString()) - .put("assigneeId", assignee.getId().toString()) - .put("subtype", "ASSIGN")) - .build(); - try { - alarmCommentService.saveAlarmComment(alarm, alarmComment, user); - } catch (ThingsboardException e) { - log.error("Failed to save alarm comment", e); - } - notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGNED, user); + String systemComment = String.format("Alarm was assigned by user %s to user %s", user.getTitle(), assignee.getTitle()); + addSystemAlarmComment(alarmInfo, user, "ASSIGN", systemComment, assignee.getId()); + notificationEntityService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, + alarmInfo.getCustomerId(), ActionType.ALARM_ASSIGNED, user); } else { throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } @@ -195,20 +165,10 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } AlarmInfo alarmInfo = result.getAlarm(); if (result.isModified()) { - AlarmComment alarmComment = AlarmComment.builder() - .alarmId(alarm.getId()) - .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned by user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString()) - .put("subtype", "ASSIGN")) - .build(); - try { - alarmCommentService.saveAlarmComment(alarm, alarmComment, user); - } catch (ThingsboardException e) { - log.error("Failed to save alarm comment", e); - } - notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGNED, user); + String systemComment = String.format("Alarm was unassigned by user %s", user.getTitle()); + addSystemAlarmComment(alarmInfo, user, "ASSIGN", systemComment); + notificationEntityService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, + alarmInfo.getCustomerId(), ActionType.ALARM_UNASSIGNED, user); } else { throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } @@ -216,48 +176,73 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } @Override - public void unassignUserAlarms(TenantId tenantId, User user, long unassignTs) { - AlarmQueryV2 alarmQuery = AlarmQueryV2.builder().assigneeId(user.getId()).pageLink(new TimePageLink(Integer.MAX_VALUE)).build(); - try { - List alarms = alarmService.findAlarmsV2(tenantId, alarmQuery).get(30, TimeUnit.SECONDS).getData(); - for (AlarmInfo alarm : alarms) { - AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(tenantId, alarm.getId(), getOrDefault(unassignTs)); - if (!result.isSuccessful()) { - continue; - } - if (result.isModified()) { - AlarmComment alarmComment = AlarmComment.builder() - .alarmId(alarm.getId()) - .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned because user %s - was deleted", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString()) - .put("subtype", "ASSIGN")) - .build(); - try { - alarmCommentService.saveAlarmComment(alarm, alarmComment, user); - } catch (ThingsboardException e) { - log.error("Failed to save alarm comment", e); - } - notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGNED, user); - } + public List unassignDeletedUserAlarms(TenantId tenantId, User user, long unassignTs) { + List totalAlarmIds = new ArrayList<>(); + PageLink pageLink = new PageLink(100, 0, null, new SortOrder("id", SortOrder.Direction.ASC)); + while (true) { + PageData pageData = alarmService.findAlarmIdsByAssigneeId(user.getTenantId(), user.getId(), pageLink); + List alarmIds = pageData.getData(); + if (alarmIds.isEmpty()) { + break; } - - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new RuntimeException(e); + processAlarmsUnassignment(tenantId, user, alarmIds, unassignTs); + totalAlarmIds.addAll(alarmIds); + pageLink = pageLink.nextPageLink(); } + return totalAlarmIds; } @Override public Boolean delete(Alarm alarm, User user) { TenantId tenantId = alarm.getTenantId(); - List relatedEdgeIds = edgeService.findAllRelatedEdgeIds(tenantId, alarm.getOriginator()); - notificationEntityService.notifyDeleteAlarm(tenantId, alarm, alarm.getOriginator(), alarm.getCustomerId(), - relatedEdgeIds, user, JacksonUtil.toString(alarm)); + notificationEntityService.logEntityAction(tenantId, alarm.getOriginator(), alarm, alarm.getCustomerId(), + ActionType.DELETED, user); return alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId()); } private static long getOrDefault(long ts) { return ts > 0 ? ts : System.currentTimeMillis(); } + + private void processAlarmsUnassignment(TenantId tenantId, User user, List alarmIds, long unassignTs) { + for (AlarmId alarmId : alarmIds) { + log.trace("[{}] Unassigning alarm {} userId {}", tenantId, alarmId, user.getId()); + AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(user.getTenantId(), alarmId, unassignTs); + if (!result.isSuccessful()) { + log.error("[{}] Cannot unassign alarm {} userId {}", tenantId, alarmId, user.getId()); + continue; + } + if (result.isModified()) { + String comment = String.format("Alarm was unassigned because user %s - was deleted", user.getTitle()); + addSystemAlarmComment(result.getAlarm(), null, "ASSIGN", comment); + notificationEntityService.logEntityAction(result.getAlarm().getTenantId(), result.getAlarm().getOriginator(), result.getAlarm(), result.getAlarm().getCustomerId(), ActionType.ALARM_UNASSIGNED, null); + } + } + } + + private void addSystemAlarmComment(Alarm alarm, User user, String subType, String commentText) { + addSystemAlarmComment(alarm, user, subType, commentText, null); + } + + private void addSystemAlarmComment(Alarm alarm, User user, String subType, String commentText, UserId assigneeId) { + ObjectNode commentNode = JacksonUtil.newObjectNode(); + commentNode.put("text", commentText) + .put("subtype", subType); + if (user != null) { + commentNode.put("userId", user.getId().getId().toString()); + } + if (assigneeId != null) { + commentNode.put("assigneeId", assigneeId.getId().toString()); + } + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(commentNode) + .build(); + try { + alarmCommentService.saveAlarmComment(alarm, alarmComment, user); + } catch (ThingsboardException e) { + log.error("Failed to save alarm comment", e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java index 24af185539..24af55f38b 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -19,9 +19,12 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import java.util.List; + public interface TbAlarmService { Alarm save(Alarm entity, User user) throws ThingsboardException; @@ -38,7 +41,7 @@ public interface TbAlarmService { AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException; - void unassignUserAlarms(TenantId tenantId, User user, long unassignTs); + List unassignDeletedUserAlarms(TenantId tenantId, User user, long unassignTs); Boolean delete(Alarm alarm, User user); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index ab57ecd050..6510ecfb7f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -36,8 +36,6 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.profile.TbAssetProfileCache; -import java.util.List; - import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; @Service @@ -62,8 +60,8 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb } Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); autoCommit(user, savedAsset.getId()); - notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedAsset.getId(), savedAsset, - asset.getCustomerId(), actionType, user); + notificationEntityService.logEntityAction(tenantId, savedAsset.getId(), savedAsset, asset.getCustomerId(), + actionType, user); tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedAsset.getId(), asset.getId() == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); return savedAsset; @@ -75,17 +73,16 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb @Override public ListenableFuture delete(Asset asset, User user) { + ActionType actionType = ActionType.DELETED; TenantId tenantId = asset.getTenantId(); AssetId assetId = asset.getId(); try { - List relatedEdgeIds = edgeService.findAllRelatedEdgeIds(tenantId, assetId); assetService.deleteAsset(tenantId, assetId); - notificationEntityService.notifyDeleteEntity(tenantId, assetId, asset, asset.getCustomerId(), - ActionType.DELETED, relatedEdgeIds, user, assetId.toString()); + notificationEntityService.logEntityAction(tenantId, assetId, asset, asset.getCustomerId(), actionType, user, assetId.toString()); tbClusterService.broadcastEntityStateChangeEvent(tenantId, assetId, ComponentLifecycleEvent.DELETED); return removeAlarmsByEntityId(tenantId, assetId); } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET), ActionType.DELETED, user, e, + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET), actionType, user, e, assetId.toString()); throw e; } @@ -97,8 +94,8 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb CustomerId customerId = customer.getId(); try { Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(tenantId, assetId, customerId)); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, customerId, savedAsset, - actionType, user, assetId.toString(), customerId.toString(), customer.getName()); + notificationEntityService.logEntityAction(tenantId, assetId, savedAsset, customerId, actionType, user, + assetId.toString(), customerId.toString(), customer.getName()); return savedAsset; } catch (Exception e) { @@ -114,8 +111,8 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb try { Asset savedAsset = checkNotNull(assetService.unassignAssetFromCustomer(tenantId, assetId)); CustomerId customerId = customer.getId(); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, customerId, savedAsset, - actionType, user, assetId.toString(), customerId.toString(), customer.getName()); + notificationEntityService.logEntityAction(tenantId, assetId, savedAsset, customerId, actionType, user, + assetId.toString(), customerId.toString(), customer.getName()); return savedAsset; } catch (Exception e) { @@ -130,8 +127,9 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb try { Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(tenantId, assetId, publicCustomer.getId())); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, savedAsset.getCustomerId(), savedAsset, - actionType, user, assetId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); + CustomerId customerId = publicCustomer.getId(); + notificationEntityService.logEntityAction(tenantId, assetId, savedAsset, customerId, actionType, user, + assetId.toString(), customerId.toString(), publicCustomer.getName()); return savedAsset; } catch (Exception e) { @@ -146,9 +144,8 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb EdgeId edgeId = edge.getId(); try { Asset savedAsset = checkNotNull(assetService.assignAssetToEdge(tenantId, assetId, edgeId)); - notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, assetId, savedAsset.getCustomerId(), - edgeId, savedAsset, actionType, user, assetId.toString(), edgeId.toString(), edge.getName()); - + notificationEntityService.logEntityAction(tenantId, assetId, savedAsset, savedAsset.getCustomerId(), + actionType, user, assetId.toString(), edgeId.toString(), edge.getName()); return savedAsset; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET), actionType, @@ -164,9 +161,8 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb EdgeId edgeId = edge.getId(); try { Asset savedAsset = checkNotNull(assetService.unassignAssetFromEdge(tenantId, assetId, edgeId)); - - notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, assetId, asset.getCustomerId(), - edgeId, asset, actionType, user, assetId.toString(), edgeId.toString(), edge.getName()); + notificationEntityService.logEntityAction(tenantId, assetId, asset, asset.getCustomerId(), + actionType, user, assetId.toString(), edgeId.toString(), edge.getName()); return savedAsset; } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java index b662994451..00b181a52a 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java @@ -59,8 +59,8 @@ public class DefaultTbAssetProfileService extends AbstractTbEntityService implem tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedAssetProfile.getId(), actionType.equals(ActionType.ADDED) ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, savedAssetProfile.getId(), - savedAssetProfile, user, actionType, true, null); + notificationEntityService.logEntityAction(tenantId, savedAssetProfile.getId(), savedAssetProfile, + null, actionType, user); return savedAssetProfile; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET_PROFILE), assetProfile, actionType, user, e); @@ -70,16 +70,17 @@ public class DefaultTbAssetProfileService extends AbstractTbEntityService implem @Override public void delete(AssetProfile assetProfile, User user) { + ActionType actionType = ActionType.DELETED; AssetProfileId assetProfileId = assetProfile.getId(); TenantId tenantId = assetProfile.getTenantId(); try { assetProfileService.deleteAssetProfile(tenantId, assetProfileId); tbClusterService.broadcastEntityStateChangeEvent(tenantId, assetProfileId, ComponentLifecycleEvent.DELETED); - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, assetProfileId, assetProfile, - user, ActionType.DELETED, true, null, assetProfileId.toString()); + notificationEntityService.logEntityAction(tenantId, assetProfileId, assetProfile, null, + actionType, user, assetProfileId.toString()); } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET_PROFILE), ActionType.DELETED, + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET_PROFILE), actionType, user, e, assetProfileId.toString()); throw e; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java index b71cfd222e..96611bb3b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java @@ -22,13 +22,10 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; -import java.util.List; - @Service @AllArgsConstructor public class DefaultTbCustomerService extends AbstractTbEntityService implements TbCustomerService { @@ -40,7 +37,7 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements try { Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer)); autoCommit(user, savedCustomer.getId()); - notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user); + notificationEntityService.logEntityAction(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user); return savedCustomer; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.CUSTOMER), customer, actionType, user, e); @@ -50,17 +47,17 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements @Override public void delete(Customer customer, User user) { + ActionType actionType = ActionType.DELETED; TenantId tenantId = customer.getTenantId(); CustomerId customerId = customer.getId(); try { - List relatedEdgeIds = edgeService.findAllRelatedEdgeIds(tenantId, customer.getId()); customerService.deleteCustomer(tenantId, customerId); - notificationEntityService.notifyDeleteEntity(tenantId, customer.getId(), customer, customerId, - ActionType.DELETED, relatedEdgeIds, user, customerId.toString()); + notificationEntityService.logEntityAction(tenantId, customer.getId(), customer, customerId, actionType, + user, customerId.toString()); tbClusterService.broadcastEntityStateChangeEvent(tenantId, customer.getId(), ComponentLifecycleEvent.DELETED); } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.CUSTOMER), ActionType.DELETED, - user, e, customerId.toString()); + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.CUSTOMER), actionType, user, + e, customerId.toString()); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java index fa5b967bd0..70918486a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java @@ -34,7 +34,6 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import java.util.HashSet; -import java.util.List; import java.util.Set; @Service @@ -51,8 +50,8 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement try { Dashboard savedDashboard = checkNotNull(dashboardService.saveDashboard(dashboard)); autoCommit(user, savedDashboard.getId()); - notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedDashboard.getId(), savedDashboard, - null, actionType, user); + notificationEntityService.logEntityAction(tenantId, savedDashboard.getId(), savedDashboard, null, + actionType, user); return savedDashboard; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), dashboard, actionType, user, e); @@ -62,15 +61,14 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement @Override public void delete(Dashboard dashboard, User user) { + ActionType actionType = ActionType.DELETED; DashboardId dashboardId = dashboard.getId(); TenantId tenantId = dashboard.getTenantId(); try { - List relatedEdgeIds = edgeService.findAllRelatedEdgeIds(tenantId, dashboardId); dashboardService.deleteDashboard(tenantId, dashboardId); - notificationEntityService.notifyDeleteEntity(tenantId, dashboardId, dashboard, null, - ActionType.DELETED, relatedEdgeIds, user, dashboardId.toString()); + notificationEntityService.logEntityAction(tenantId, dashboardId, dashboard, null, actionType, user, dashboardId.toString()); } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), ActionType.DELETED, user, e, dashboardId.toString()); + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), actionType, user, e, dashboardId.toString()); throw e; } } @@ -83,8 +81,8 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement DashboardId dashboardId = dashboard.getId(); try { Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId)); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard, - actionType, user, dashboardId.toString(), customerId.toString(), customer.getName()); + notificationEntityService.logEntityAction(tenantId, dashboardId, savedDashboard, customerId, actionType, + user, dashboardId.toString(), customerId.toString(), customer.getName()); return savedDashboard; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), actionType, @@ -101,9 +99,8 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement try { Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, publicCustomer.getId())); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, publicCustomer.getId(), savedDashboard, - actionType, user, dashboardId.toString(), - publicCustomer.getId().toString(), publicCustomer.getName()); + notificationEntityService.logEntityAction(tenantId, dashboardId, savedDashboard, publicCustomer.getId(), + actionType, user, dashboardId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedDashboard; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), actionType, user, e, dashboardId.toString()); @@ -119,9 +116,8 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement try { Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, publicCustomer.getId())); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, publicCustomer.getId(), dashboard, - actionType, user, dashboardId.toString(), - publicCustomer.getId().toString(), publicCustomer.getName()); + notificationEntityService.logEntityAction(tenantId, dashboardId, dashboard, publicCustomer.getId(), actionType, + user, dashboardId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedDashboard; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), actionType, user, e, dashboardId.toString()); @@ -159,15 +155,15 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement for (CustomerId customerId : addedCustomerIds) { savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId)); ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, savedDashboard.getId(), customerId, savedDashboard, + notificationEntityService.logEntityAction(tenantId, savedDashboard.getId(), savedDashboard, customerId, actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); } actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; for (CustomerId customerId : removedCustomerIds) { ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, customerId)); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, savedDashboard.getId(), customerId, savedDashboard, - ActionType.UNASSIGNED_FROM_CUSTOMER, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); + notificationEntityService.logEntityAction(tenantId, savedDashboard.getId(), savedDashboard, customerId, + actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); } return savedDashboard; } @@ -196,7 +192,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement for (CustomerId customerId : addedCustomerIds) { savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId)); ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard, + notificationEntityService.logEntityAction(tenantId, dashboardId, savedDashboard, customerId, actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); } return savedDashboard; @@ -226,7 +222,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement for (CustomerId customerId : removedCustomerIds) { ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, customerId)); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard, + notificationEntityService.logEntityAction(tenantId, dashboardId, savedDashboard, customerId, actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); } return savedDashboard; @@ -243,9 +239,8 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement EdgeId edgeId = edge.getId(); try { Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToEdge(tenantId, dashboardId, edgeId)); - notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, dashboardId, null, - edgeId, savedDashboard, actionType, user, dashboardId.toString(), - edgeId.toString(), edge.getName()); + notificationEntityService.logEntityAction(tenantId, dashboardId, savedDashboard, null, actionType, + user, dashboardId.toString(), edgeId.toString(), edge.getName()); return savedDashboard; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DEVICE), @@ -262,10 +257,8 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement EdgeId edgeId = edge.getId(); try { Dashboard savedDevice = checkNotNull(dashboardService.unassignDashboardFromEdge(tenantId, dashboardId, edgeId)); - - notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, dashboardId, null, - edgeId, dashboard, actionType, user, dashboardId.toString(), - edgeId.toString(), edge.getName()); + notificationEntityService.logEntityAction(tenantId, dashboardId, dashboard, null, actionType, + user, dashboardId.toString(), edgeId.toString(), edge.getName()); return savedDevice; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), actionType, user, e, @@ -281,7 +274,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement DashboardId dashboardId = dashboard.getId(); try { Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, customer.getId())); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customer.getId(), savedDashboard, + notificationEntityService.logEntityAction(tenantId, dashboardId, savedDashboard, customer.getId(), actionType, user, dashboardId.toString(), customer.getId().toString(), customer.getName()); return savedDashboard; } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java index 2ab8de9438..544a199498 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java @@ -44,8 +44,6 @@ import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; -import java.util.List; - @AllArgsConstructor @TbCoreComponent @Service @@ -97,10 +95,9 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T TenantId tenantId = device.getTenantId(); DeviceId deviceId = device.getId(); try { - List relatedEdgeIds = edgeService.findAllRelatedEdgeIds(tenantId, deviceId); deviceService.deleteDevice(tenantId, deviceId); notificationEntityService.notifyDeleteDevice(tenantId, deviceId, device.getCustomerId(), device, - relatedEdgeIds, user, deviceId.toString()); + user, deviceId.toString()); return removeAlarmsByEntityId(tenantId, deviceId); } catch (Exception e) { @@ -116,8 +113,8 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T CustomerId customerId = customer.getId(); try { Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(tenantId, deviceId, customerId)); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, customerId, savedDevice, - actionType, user, deviceId.toString(), customerId.toString(), customer.getName()); + notificationEntityService.logEntityAction(tenantId, deviceId, savedDevice, customerId, actionType, user, + deviceId.toString(), customerId.toString(), customer.getName()); return savedDevice; } catch (Exception e) { @@ -136,8 +133,8 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T Device savedDevice = checkNotNull(deviceService.unassignDeviceFromCustomer(tenantId, deviceId)); CustomerId customerId = customer.getId(); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, customerId, savedDevice, - actionType, user, deviceId.toString(), customerId.toString(), customer.getName()); + notificationEntityService.logEntityAction(tenantId, deviceId, savedDevice, customerId, actionType, user, + deviceId.toString(), customerId.toString(), customer.getName()); return savedDevice; } catch (Exception e) { @@ -154,9 +151,8 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T try { Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(tenantId, deviceId, publicCustomer.getId())); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, savedDevice.getCustomerId(), savedDevice, - actionType, user, deviceId.toString(), - publicCustomer.getId().toString(), publicCustomer.getName()); + notificationEntityService.logEntityAction(tenantId, deviceId, savedDevice, savedDevice.getCustomerId(), + actionType, user, deviceId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedDevice; } catch (Exception e) { @@ -252,30 +248,32 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T EdgeId edgeId = edge.getId(); try { Device savedDevice = checkNotNull(deviceService.assignDeviceToEdge(tenantId, deviceId, edgeId)); - notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, deviceId, savedDevice.getCustomerId(), - edgeId, savedDevice, actionType, user, deviceId.toString(), edgeId.toString(), edge.getName()); + notificationEntityService.logEntityAction(tenantId, deviceId, savedDevice, savedDevice.getCustomerId(), + actionType, user, deviceId.toString(), edgeId.toString(), edge.getName()); + return savedDevice; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DEVICE), - ActionType.ASSIGNED_TO_EDGE, user, e, deviceId.toString(), edgeId.toString()); + actionType, user, e, deviceId.toString(), edgeId.toString()); throw e; } } @Override public Device unassignDeviceFromEdge(Device device, Edge edge, User user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_EDGE; TenantId tenantId = device.getTenantId(); DeviceId deviceId = device.getId(); EdgeId edgeId = edge.getId(); try { Device savedDevice = checkNotNull(deviceService.unassignDeviceFromEdge(tenantId, deviceId, edgeId)); + notificationEntityService.logEntityAction(tenantId, deviceId, savedDevice, savedDevice.getCustomerId(), + actionType, user, deviceId.toString(), edgeId.toString(), edge.getName()); - notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, deviceId, device.getCustomerId(), - edgeId, device, ActionType.UNASSIGNED_FROM_EDGE, user, deviceId.toString(), edgeId.toString(), edge.getName()); return savedDevice; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DEVICE), - ActionType.UNASSIGNED_FROM_EDGE, user, e, deviceId.toString(), edgeId.toString()); + actionType, user, e, deviceId.toString(), edgeId.toString()); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/DefaultTbDeviceProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/DefaultTbDeviceProfileService.java index faa6a69983..70ee722493 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/DefaultTbDeviceProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/DefaultTbDeviceProfileService.java @@ -67,8 +67,8 @@ public class DefaultTbDeviceProfileService extends AbstractTbEntityService imple otaPackageStateService.update(savedDeviceProfile, isFirmwareChanged, isSoftwareChanged); - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, savedDeviceProfile.getId(), - savedDeviceProfile, user, actionType, true, null); + notificationEntityService.logEntityAction(tenantId, savedDeviceProfile.getId(), savedDeviceProfile, + null, actionType, user); return savedDeviceProfile; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DEVICE_PROFILE), deviceProfile, actionType, user, e); @@ -78,6 +78,7 @@ public class DefaultTbDeviceProfileService extends AbstractTbEntityService imple @Override public void delete(DeviceProfile deviceProfile, User user) { + ActionType actionType = ActionType.DELETED; DeviceProfileId deviceProfileId = deviceProfile.getId(); TenantId tenantId = deviceProfile.getTenantId(); try { @@ -85,10 +86,10 @@ public class DefaultTbDeviceProfileService extends AbstractTbEntityService imple tbClusterService.onDeviceProfileDelete(deviceProfile, null); tbClusterService.broadcastEntityStateChangeEvent(tenantId, deviceProfileId, ComponentLifecycleEvent.DELETED); - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, deviceProfileId, deviceProfile, - user, ActionType.DELETED, true, null, deviceProfileId.toString()); + notificationEntityService.logEntityAction(tenantId, deviceProfileId, deviceProfile, null, + actionType, user, deviceProfileId.toString()); } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DEVICE_PROFILE), ActionType.DELETED, + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DEVICE_PROFILE), actionType, user, e, deviceProfileId.toString()); throw e; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java index c51ae3352e..adc20c21d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java @@ -89,8 +89,8 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE CustomerId customerId = customer.getId(); try { Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(tenantId, edgeId, customerId)); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, edgeId, customerId, savedEdge, - actionType, user, edgeId.toString(), customerId.toString(), customer.getName()); + notificationEntityService.logEntityAction(tenantId, edgeId, savedEdge, customerId, actionType, + user, edgeId.toString(), customerId.toString(), customer.getName()); return savedEdge; } catch (Exception e) { @@ -108,8 +108,8 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE CustomerId customerId = customer.getId(); try { Edge savedEdge = checkNotNull(edgeService.unassignEdgeFromCustomer(tenantId, edgeId)); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, edgeId, customerId, savedEdge, - actionType, user, edgeId.toString(), customerId.toString(), customer.getName()); + notificationEntityService.logEntityAction(tenantId, edgeId, savedEdge, customerId, actionType, + user, edgeId.toString(), customerId.toString(), customer.getName()); return savedEdge; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.EDGE), diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java index b978b730fd..904d98653e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java @@ -40,41 +40,38 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl @Override public void save(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException { + ActionType actionType = ActionType.RELATION_ADD_OR_UPDATE; try { relationService.saveRelation(tenantId, relation); - notificationEntityService.notifyRelation(tenantId, customerId, - relation, user, ActionType.RELATION_ADD_OR_UPDATE, relation); + notificationEntityService.logEntityRelationAction(tenantId, customerId, + relation, user, actionType, null, relation); } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, relation.getFrom(), null, customerId, - ActionType.RELATION_ADD_OR_UPDATE, user, e, relation); - notificationEntityService.logEntityAction(tenantId, relation.getTo(), null, customerId, - ActionType.RELATION_ADD_OR_UPDATE, user, e, relation); + notificationEntityService.logEntityRelationAction(tenantId, customerId, + relation, user, actionType, e, relation); throw e; } } @Override public void delete(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException { + ActionType actionType = ActionType.RELATION_DELETED; try { boolean found = relationService.deleteRelation(tenantId, relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup()); if (!found) { throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND); } - notificationEntityService.notifyRelation(tenantId, customerId, - relation, user, ActionType.RELATION_DELETED, relation); + notificationEntityService.logEntityRelationAction(tenantId, customerId, relation, user, actionType, null, relation); } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, relation.getFrom(), null, customerId, - ActionType.RELATION_DELETED, user, e, relation); - notificationEntityService.logEntityAction(tenantId, relation.getTo(), null, customerId, - ActionType.RELATION_DELETED, user, e, relation); + notificationEntityService.logEntityRelationAction(tenantId, customerId, + relation, user, actionType, e, relation); throw e; } } @Override - public void deleteRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException { + public void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException { try { - relationService.deleteEntityRelations(tenantId, entityId); + relationService.deleteEntityCommonRelations(tenantId, entityId); notificationEntityService.logEntityAction(tenantId, entityId, null, customerId, ActionType.RELATIONS_DELETED, user); } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, entityId, null, customerId, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java index 2caee86d0c..8bf5018c95 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java @@ -28,6 +28,6 @@ public interface TbEntityRelationService { void delete(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException; - void deleteRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException; + void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java index 48e6c4b634..f587e19ef2 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java @@ -81,7 +81,7 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView)); this.updateEntityViewAttributes(tenantId, savedEntityView, existingEntityView, user); autoCommit(user, savedEntityView.getId()); - notificationEntityService.notifyCreateOrUpdateEntity(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView, + notificationEntityService.logEntityAction(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView, null, actionType, user); localCache.computeIfAbsent(savedEntityView.getTenantId(), (k) -> new ConcurrentReferenceHashMap<>()).clear(); tbClusterService.broadcastEntityStateChangeEvent(savedEntityView.getTenantId(), savedEntityView.getId(), @@ -129,10 +129,9 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen TenantId tenantId = entityView.getTenantId(); EntityViewId entityViewId = entityView.getId(); try { - List relatedEdgeIds = edgeService.findAllRelatedEdgeIds(tenantId, entityViewId); entityViewService.deleteEntityView(tenantId, entityViewId); - notificationEntityService.notifyDeleteEntity(tenantId, entityViewId, entityView, entityView.getCustomerId(), ActionType.DELETED, - relatedEdgeIds, user, entityViewId.toString()); + notificationEntityService.logEntityAction(tenantId, entityViewId, entityView, entityView.getCustomerId(), + ActionType.DELETED, user, entityViewId.toString()); localCache.computeIfAbsent(tenantId, (k) -> new ConcurrentReferenceHashMap<>()).clear(); tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityViewId, ComponentLifecycleEvent.DELETED); @@ -145,15 +144,31 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen @Override public EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer, User user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; CustomerId customerId = customer.getId(); try { EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(tenantId, entityViewId, customerId)); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customerId, savedEntityView, - ActionType.ASSIGNED_TO_CUSTOMER, user, entityViewId.toString(), customerId.toString(), customer.getName()); + notificationEntityService.logEntityAction(tenantId, entityViewId, savedEntityView, savedEntityView.getCustomerId(), + actionType, user, entityViewId.toString(), customerId.toString(), customer.getName()); + return savedEntityView; + } catch (Exception e) { + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW), + actionType, user, e, entityViewId.toString(), customerId.toString()); + throw e; + } + } + + @Override + public EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer, User user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; + try { + EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromCustomer(tenantId, entityViewId)); + notificationEntityService.logEntityAction(tenantId, entityViewId, savedEntityView, customer.getId(), + actionType, user, savedEntityView.getId().toString(), customer.getId().toString(), customer.getName()); return savedEntityView; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW), - ActionType.ASSIGNED_TO_CUSTOMER, user, e, entityViewId.toString(), customerId.toString()); + actionType, user, e, entityViewId.toString()); throw e; } } @@ -165,9 +180,8 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen try { EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(tenantId, entityViewId, publicCustomer.getId())); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, savedEntityView.getCustomerId(), savedEntityView, - actionType, user, savedEntityView.getId().toString(), - publicCustomer.getId().toString(), publicCustomer.getName()); + notificationEntityService.logEntityAction(tenantId, entityViewId, savedEntityView, savedEntityView.getCustomerId(), + actionType, user, savedEntityView.getId().toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedEntityView; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW), @@ -178,16 +192,16 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen @Override public EntityView assignEntityViewToEdge(TenantId tenantId, CustomerId customerId, EntityViewId entityViewId, Edge edge, User user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_EDGE; EdgeId edgeId = edge.getId(); try { EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToEdge(tenantId, entityViewId, edgeId)); - notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, entityViewId, customerId, - edgeId, savedEntityView, ActionType.ASSIGNED_TO_EDGE, user, savedEntityView.getEntityId().toString(), - edgeId.toString(), edge.getName()); + notificationEntityService.logEntityAction(tenantId, entityViewId, savedEntityView, customerId, actionType, + user, savedEntityView.getEntityId().toString(), edgeId.toString(), edge.getName()); return savedEntityView; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW), - ActionType.ASSIGNED_TO_EDGE, user, e, entityViewId.toString(), edgeId.toString()); + actionType, user, e, entityViewId.toString(), edgeId.toString()); throw e; } } @@ -195,34 +209,17 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen @Override public EntityView unassignEntityViewFromEdge(TenantId tenantId, CustomerId customerId, EntityView entityView, Edge edge, User user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_EDGE; EntityViewId entityViewId = entityView.getId(); EdgeId edgeId = edge.getId(); try { EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromEdge(tenantId, entityViewId, edgeId)); - notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, entityViewId, customerId, - edgeId, entityView, ActionType.UNASSIGNED_FROM_EDGE, user, entityViewId.toString(), - edgeId.toString(), edge.getName()); + notificationEntityService.logEntityAction(tenantId, entityViewId, savedEntityView, customerId, actionType, + user, entityViewId.toString(), edgeId.toString(), edge.getName()); return savedEntityView; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW), - ActionType.UNASSIGNED_FROM_EDGE, user, e, entityViewId.toString(), edgeId.toString()); - throw e; - } - } - - @Override - public EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer, User user) throws ThingsboardException { - ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; - try { - EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromCustomer(tenantId, entityViewId)); - - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customer.getId(), savedEntityView, - actionType, user, savedEntityView.getId().toString(), customer.getId().toString(), customer.getName()); - - return savedEntityView; - } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW), - actionType, user, e, entityViewId.toString()); + actionType, user, e, entityViewId.toString(), edgeId.toString()); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java index 4d3c61e615..3cc444caf5 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java @@ -50,9 +50,8 @@ public class DefaultTbOtaPackageService extends AbstractTbEntityService implemen try { OtaPackageInfo savedOtaPackageInfo = otaPackageService.saveOtaPackageInfo(new OtaPackageInfo(saveOtaPackageInfoRequest), saveOtaPackageInfoRequest.isUsesUrl()); - boolean sendMsgToEdge = savedOtaPackageInfo.hasUrl() || savedOtaPackageInfo.isHasData(); - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, savedOtaPackageInfo.getId(), - savedOtaPackageInfo, user, actionType, sendMsgToEdge, null); + notificationEntityService.logEntityAction(tenantId, savedOtaPackageInfo.getId(), savedOtaPackageInfo, + null, actionType, user); return savedOtaPackageInfo; } catch (Exception e) { @@ -65,6 +64,7 @@ public class DefaultTbOtaPackageService extends AbstractTbEntityService implemen @Override public OtaPackageInfo saveOtaPackageData(OtaPackageInfo otaPackageInfo, String checksum, ChecksumAlgorithm checksumAlgorithm, byte[] data, String filename, String contentType, User user) throws ThingsboardException { + ActionType actionType = ActionType.UPDATED; TenantId tenantId = otaPackageInfo.getTenantId(); OtaPackageId otaPackageId = otaPackageInfo.getId(); try { @@ -87,27 +87,26 @@ public class DefaultTbOtaPackageService extends AbstractTbEntityService implemen otaPackage.setData(ByteBuffer.wrap(data)); otaPackage.setDataSize((long) data.length); OtaPackageInfo savedOtaPackage = otaPackageService.saveOtaPackage(otaPackage); - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, savedOtaPackage.getId(), - savedOtaPackage, user, ActionType.UPDATED, true, null); + notificationEntityService.logEntityAction(tenantId, savedOtaPackage.getId(), savedOtaPackage, null, actionType, user); return savedOtaPackage; } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.OTA_PACKAGE), ActionType.UPDATED, - user, e, otaPackageId.toString()); + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.OTA_PACKAGE), actionType, user, e, otaPackageId.toString()); throw e; } } @Override public void delete(OtaPackageInfo otaPackageInfo, User user) throws ThingsboardException { + ActionType actionType = ActionType.DELETED; TenantId tenantId = otaPackageInfo.getTenantId(); OtaPackageId otaPackageId = otaPackageInfo.getId(); try { otaPackageService.deleteOtaPackage(tenantId, otaPackageId); - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, otaPackageId, otaPackageInfo, - user, ActionType.DELETED, true, null, otaPackageInfo.getId().toString()); + notificationEntityService.logEntityAction(tenantId, otaPackageId, otaPackageInfo, null, + actionType, user, otaPackageInfo.getId().toString()); } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.OTA_PACKAGE), - ActionType.DELETED, user, e, otaPackageId.toString()); + actionType, user, e, otaPackageId.toString()); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java index 63e11aeb7a..b0ef8f4cde 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java @@ -15,12 +15,11 @@ */ package org.thingsboard.server.service.entitiy.queue; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; @@ -28,7 +27,6 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfi import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.queue.TbQueueAdmin; -import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -36,20 +34,17 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Slf4j @Service @TbCoreComponent -@AllArgsConstructor +@RequiredArgsConstructor public class DefaultTbQueueService extends AbstractTbEntityService implements TbQueueService { - private static final long DELETE_DELAY = 30; private final QueueService queueService; private final TbClusterService tbClusterService; private final TbQueueAdmin tbQueueAdmin; - private final SchedulerComponent scheduler; @Override public Queue saveQueue(Queue queue) { @@ -71,8 +66,6 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb onQueueUpdated(savedQueue, oldQueue); } - notificationEntityService.notifySendMsgToEdgeService(queue.getTenantId(), savedQueue.getId(), create ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); - return savedQueue; } @@ -93,7 +86,9 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb private void onQueueCreated(Queue queue) { for (int i = 0; i < queue.getPartitions(); i++) { tbQueueAdmin.createTopicIfNotExists( - new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); + new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(), + queue.getCustomProperties() + ); } tbClusterService.onQueueChange(queue); @@ -108,21 +103,15 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb log.info("Added [{}] new partitions to [{}] queue", currentPartitions - oldPartitions, queue.getName()); for (int i = oldPartitions; i < currentPartitions; i++) { tbQueueAdmin.createTopicIfNotExists( - new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); + new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(), + queue.getCustomProperties() + ); } tbClusterService.onQueueChange(queue); } else { log.info("Removed [{}] partitions from [{}] queue", oldPartitions - currentPartitions, queue.getName()); tbClusterService.onQueueChange(queue); - - scheduler.schedule(() -> { - for (int i = currentPartitions; i < oldPartitions; i++) { - String fullTopicName = new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(); - log.info("Removed partition [{}]", fullTopicName); - tbQueueAdmin.deleteTopic( - fullTopicName); - } - }, DELETE_DELAY, TimeUnit.SECONDS); + // TODO: move all the messages left in old partitions and delete topics } } else if (!oldQueue.equals(queue)) { tbClusterService.onQueueChange(queue); @@ -131,22 +120,7 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb private void onQueueDeleted(Queue queue) { tbClusterService.onQueueDelete(queue); - // queueStatsService.deleteQueueStatsByQueueId(tenantId, queueId); - - scheduler.schedule(() -> { - for (int i = 0; i < queue.getPartitions(); i++) { - String fullTopicName = new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(); - log.info("Deleting queue [{}]", fullTopicName); - try { - tbQueueAdmin.deleteTopic(fullTopicName); - } catch (Exception e) { - log.error("Failed to delete queue [{}]", fullTopicName); - } - } - }, DELETE_DELAY, TimeUnit.SECONDS); - - notificationEntityService.notifySendMsgToEdgeService(queue.getTenantId(), queue.getId(), EdgeEventActionType.DELETED); } @Override @@ -198,6 +172,10 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb } } + if (log.isDebugEnabled()) { + log.debug("[{}] Handling profile queue config update: creating queues {}, updating {}, deleting {}. Affected tenants: {}", + newTenantProfile.getUuidId(), toCreate, toUpdate, toRemove, tenantIds); + } tenantIds.forEach(tenantId -> { toCreate.forEach(key -> saveQueue(new Queue(tenantId, newQueues.get(key)))); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java index 1219a5b929..15bcc2e040 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java @@ -44,16 +44,14 @@ public class DefaultTbTenantProfileService extends AbstractTbEntityService imple @Override public TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile) throws ThingsboardException { TenantProfile savedTenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(tenantId, tenantProfile)); - if (oldTenantProfile != null && savedTenantProfile.isIsolatedTbRuleEngine()) { - List tenantIds = tenantService.findTenantIdsByTenantProfileId(savedTenantProfile.getId()); - tbQueueService.updateQueuesByTenants(tenantIds, savedTenantProfile, oldTenantProfile); - } - tenantProfileCache.put(savedTenantProfile); tbClusterService.onTenantProfileChange(savedTenantProfile, null); tbClusterService.broadcastEntityStateChangeEvent(TenantId.SYS_TENANT_ID, savedTenantProfile.getId(), tenantProfile.getId() == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + List tenantIds = tenantService.findTenantIdsByTenantProfileId(savedTenantProfile.getId()); + tbQueueService.updateQueuesByTenants(tenantIds, savedTenantProfile, oldTenantProfile); + return savedTenantProfile; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java index c45d3314cd..2588f4a82c 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; public interface TbTenantProfileService { + TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile) throws ThingsboardException; void delete(TenantId tenantId, TenantProfile tenantProfile) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java index 0c04e46ff5..2c2db7c6c7 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; -import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; @@ -44,7 +43,6 @@ import static org.thingsboard.server.controller.UserController.ACTIVATE_URL_PATT public class DefaultUserService extends AbstractTbEntityService implements TbUserService { private final UserService userService; - private final TbAlarmService tbAlarmService; private final MailService mailService; private final SystemSecurityService systemSecurityService; @@ -54,7 +52,7 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse ActionType actionType = tbUser.getId() == null ? ActionType.ADDED : ActionType.UPDATED; try { boolean sendEmail = tbUser.getId() == null && sendActivationMail; - User savedUser = checkNotNull(userService.saveUser(tbUser)); + User savedUser = checkNotNull(userService.saveUser(tenantId, tbUser)); if (sendEmail) { UserCredentials userCredentials = userService.findUserCredentialsByUserId(tenantId, savedUser.getId()); String baseUrl = systemSecurityService.getBaseUrl(tenantId, customerId, request); @@ -64,12 +62,11 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse try { mailService.sendActivationEmail(activateUrl, email); } catch (ThingsboardException e) { - userService.deleteUser(tenantId, savedUser.getId()); + userService.deleteUser(tenantId, savedUser); throw e; } } - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, customerId, savedUser.getId(), - savedUser, user, actionType, true, null); + notificationEntityService.logEntityAction(tenantId, savedUser.getId(), savedUser, customerId, actionType, user); return savedUser; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.USER), tbUser, actionType, user, e); @@ -78,17 +75,16 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse } @Override - public void delete(TenantId tenantId, CustomerId customerId, User tbUser, User user) throws ThingsboardException { - UserId userId = tbUser.getId(); + public void delete(TenantId tenantId, CustomerId customerId, User user, User responsibleUser) throws ThingsboardException { + ActionType actionType = ActionType.DELETED; + UserId userId = user.getId(); try { - tbAlarmService.unassignUserAlarms(tenantId, tbUser, System.currentTimeMillis()); - userService.deleteUser(tenantId, userId); - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, customerId, userId, tbUser, - user, ActionType.DELETED, true, null, customerId.toString()); + userService.deleteUser(tenantId, user); + notificationEntityService.logEntityAction(tenantId, userId, user, customerId, actionType, responsibleUser, customerId.toString()); } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.USER), - ActionType.DELETED, user, e, userId.toString()); + actionType, responsibleUser, e, userId.toString()); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java index 3ef79371d4..0764425116 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java @@ -25,5 +25,5 @@ import javax.servlet.http.HttpServletRequest; public interface TbUserService { User save(TenantId tenantId, CustomerId customerId, User tbUser, boolean sendActivationMail, HttpServletRequest request, User user) throws ThingsboardException; - void delete(TenantId tenantId, CustomerId customerId, User tbUser, User user) throws ThingsboardException; + void delete(TenantId tenantId, CustomerId customerId, User user, User responsibleUser) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java index 7cc150b746..ffa11e8c46 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java @@ -21,17 +21,23 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import java.util.List; + @Service @TbCoreComponent @AllArgsConstructor public class DefaultWidgetsBundleService extends AbstractTbEntityService implements TbWidgetsBundleService { private final WidgetsBundleService widgetsBundleService; + private final WidgetTypeService widgetTypeService; @Override public WidgetsBundle save(WidgetsBundle widgetsBundle, User user) throws Exception { @@ -40,8 +46,8 @@ public class DefaultWidgetsBundleService extends AbstractTbEntityService impleme try { WidgetsBundle savedWidgetsBundle = checkNotNull(widgetsBundleService.saveWidgetsBundle(widgetsBundle)); autoCommit(user, savedWidgetsBundle.getId()); - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, savedWidgetsBundle.getId(), - savedWidgetsBundle, user, actionType, true, null); + notificationEntityService.logEntityAction(tenantId, savedWidgetsBundle.getId(), savedWidgetsBundle, + null, actionType, user); return savedWidgetsBundle; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.WIDGETS_BUNDLE), widgetsBundle, actionType, user, e); @@ -51,15 +57,26 @@ public class DefaultWidgetsBundleService extends AbstractTbEntityService impleme @Override public void delete(WidgetsBundle widgetsBundle, User user) { + ActionType actionType = ActionType.DELETED; TenantId tenantId = widgetsBundle.getTenantId(); try { widgetsBundleService.deleteWidgetsBundle(widgetsBundle.getTenantId(), widgetsBundle.getId()); - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, widgetsBundle.getId(), widgetsBundle, - user, ActionType.DELETED, true, null); + notificationEntityService.logEntityAction(tenantId, widgetsBundle.getId(), widgetsBundle, null, actionType, user); } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.WIDGETS_BUNDLE), - ActionType.DELETED, user, e, widgetsBundle.getId()); + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.WIDGETS_BUNDLE), actionType, user, e, widgetsBundle.getId()); throw e; } } + + @Override + public void updateWidgetsBundleWidgetTypes(WidgetsBundleId widgetsBundleId, List widgetTypeIds, User user) throws Exception { + widgetTypeService.updateWidgetsBundleWidgetTypes(user.getTenantId(), widgetsBundleId, widgetTypeIds); + autoCommit(user, widgetsBundleId); + } + + @Override + public void updateWidgetsBundleWidgetFqns(WidgetsBundleId widgetsBundleId, List widgetFqns, User user) throws Exception { + widgetTypeService.updateWidgetsBundleWidgetFqns(user.getTenantId(), widgetsBundleId, widgetFqns); + autoCommit(user, widgetsBundleId); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java index d60bc66d66..7a954cdda8 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java @@ -15,8 +15,18 @@ */ package org.thingsboard.server.service.entitiy.widgets.bundle; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.service.entitiy.SimpleTbEntityService; +import java.util.List; + public interface TbWidgetsBundleService extends SimpleTbEntityService { + + void updateWidgetsBundleWidgetTypes(WidgetsBundleId widgetsBundleId, List widgetTypeIds, User user) throws Exception; + + void updateWidgetsBundleWidgetFqns(WidgetsBundleId widgetsBundleId, List widgetFqns, User user) throws Exception; + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java new file mode 100644 index 0000000000..650f92ac20 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy.widgets.type; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; + +@Service +@TbCoreComponent +@AllArgsConstructor +public class DefaultWidgetTypeService extends AbstractTbEntityService implements TbWidgetTypeService { + + private final WidgetTypeService widgetTypeService; + + @Override + public WidgetTypeDetails save(WidgetTypeDetails entity, User user) throws Exception { + return this.save(entity, false, user); + } + + @Override + public WidgetTypeDetails save(WidgetTypeDetails widgetTypeDetails, boolean updateExistingByFqn, User user) throws Exception { + TenantId tenantId = widgetTypeDetails.getTenantId(); + if (widgetTypeDetails.getId() == null && updateExistingByFqn) { + WidgetType widgetType = widgetTypeService.findWidgetTypeByTenantIdAndFqn(tenantId, widgetTypeDetails.getFqn()); + if (widgetType != null) { + widgetTypeDetails.setId(widgetType.getId()); + } + } + ActionType actionType = widgetTypeDetails.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + try { + WidgetTypeDetails savedWidgetTypeDetails = checkNotNull(widgetTypeService.saveWidgetType(widgetTypeDetails)); + autoCommit(user, savedWidgetTypeDetails.getId()); + notificationEntityService.logEntityAction(tenantId, savedWidgetTypeDetails.getId(), savedWidgetTypeDetails, + null, actionType, user); + return savedWidgetTypeDetails; + } catch (Exception e) { + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.WIDGET_TYPE), widgetTypeDetails, actionType, user, e); + throw e; + } + } + + @Override + public void delete(WidgetTypeDetails widgetTypeDetails, User user) { + ActionType actionType = ActionType.DELETED; + TenantId tenantId = widgetTypeDetails.getTenantId(); + try { + widgetTypeService.deleteWidgetType(widgetTypeDetails.getTenantId(), widgetTypeDetails.getId()); + notificationEntityService.logEntityAction(tenantId, widgetTypeDetails.getId(), widgetTypeDetails, null, actionType, user); + } catch (Exception e) { + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.WIDGET_TYPE), actionType, user, e, widgetTypeDetails.getId()); + throw e; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/TbWidgetTypeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/TbWidgetTypeService.java new file mode 100644 index 0000000000..8951b49552 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/TbWidgetTypeService.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy.widgets.type; + +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.service.entitiy.SimpleTbEntityService; + +public interface TbWidgetTypeService extends SimpleTbEntityService { + + WidgetTypeDetails save(WidgetTypeDetails widgetTypeDetails, boolean updateExistingByFqn, User user) throws Exception; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/InMemoryHouseKeeperServiceService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/InMemoryHouseKeeperServiceService.java new file mode 100644 index 0000000000..4e83aa0060 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/InMemoryHouseKeeperServiceService.java @@ -0,0 +1,104 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.housekeeper; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.housekeeper.HouseKeeperService; +import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +@Component +@RequiredArgsConstructor +@Slf4j +public class InMemoryHouseKeeperServiceService implements HouseKeeperService { + + final TbAlarmService alarmService; + + ListeningExecutorService executor; + + AtomicInteger queueSize = new AtomicInteger(); + AtomicInteger totalProcessedCounter = new AtomicInteger(); + + @PostConstruct + public void init() { + log.debug("Starting HouseKeeper service"); + executor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper"))); + } + + @PreDestroy + public void destroy() { + if (executor != null) { + log.debug("Stopping HouseKeeper service"); + executor.shutdown(); + } + } + + @TransactionalEventListener(fallbackExecution = true) + public void handleEvent(DeleteEntityEvent event) { + log.trace("[{}] DeleteEntityEvent handler: {}", event.getTenantId(), event); + EntityId entityId = event.getEntityId(); + if (EntityType.USER.equals(entityId.getEntityType())) { + unassignDeletedUserAlarms(event.getTenantId(), (User) event.getEntity(), event.getTs()); + } + } + + @Override + public ListenableFuture> unassignDeletedUserAlarms(TenantId tenantId, User user, long unassignTs) { + log.debug("[{}][{}] unassignDeletedUserAlarms submitting, pending queue size: {} ", tenantId, user.getId().getId(), queueSize.get()); + queueSize.incrementAndGet(); + ListenableFuture> future = executor.submit(() -> alarmService.unassignDeletedUserAlarms(tenantId, user, unassignTs)); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(List alarmIds) { + queueSize.decrementAndGet(); + totalProcessedCounter.incrementAndGet(); + log.debug("[{}][{}] unassignDeletedUserAlarms finished, pending queue size: {}, total processed count: {} ", + tenantId, user.getId().getId(), queueSize.get(), totalProcessedCounter.get()); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + queueSize.decrementAndGet(); + totalProcessedCounter.incrementAndGet(); + log.error("[{}][{}] unassignDeletedUserAlarms failed, pending queue size: {}, total processed count: {}", + tenantId, user.getId().getId(), queueSize.get(), totalProcessedCounter.get(), throwable); + } + }, MoreExecutors.directExecutor()); + return future; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 7965824274..8530478c2a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -99,6 +99,7 @@ import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; @@ -114,13 +115,14 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; + @Service @Profile("install") @Slf4j public class DefaultSystemDataLoaderService implements SystemDataLoaderService { public static final String CUSTOMER_CRED = "customer"; - public static final String DEFAULT_DEVICE_TYPE = "default"; public static final String ACTIVITY_STATE = "active"; @Autowired @@ -135,6 +137,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Autowired private AdminSettingsService adminSettingsService; + @Autowired + private WidgetTypeService widgetTypeService; + @Autowired private WidgetsBundleService widgetsBundleService; @@ -484,6 +489,10 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { public void deleteSystemWidgetBundle(String bundleAlias) throws Exception { WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias); if (widgetsBundle != null) { + var widgetTypes = widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(TenantId.SYS_TENANT_ID, widgetsBundle.getId()); + for (var widgetType : widgetTypes) { + widgetTypeService.deleteWidgetType(TenantId.SYS_TENANT_ID, widgetType.getId()); + } widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, widgetsBundle.getId()); } } @@ -511,6 +520,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { this.deleteSystemWidgetBundle("navigation_widgets"); this.deleteSystemWidgetBundle("edge_widgets"); this.deleteSystemWidgetBundle("home_page_widgets"); + this.deleteSystemWidgetBundle("entity_widgets"); + this.deleteSystemWidgetBundle("html_widgets"); + this.deleteSystemWidgetBundle("tables"); installScripts.loadSystemWidgets(); } @@ -524,7 +536,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { user.setEmail(email); user.setTenantId(tenantId); user.setCustomerId(customerId); - user = userService.saveUser(user); + user = userService.saveUser(tenantId, user); UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId()); userCredentials.setPassword(passwordEncoder.encode(password)); userCredentials.setEnabled(true); 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 36ec2b9b78..40596e03e9 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 @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; @@ -46,7 +47,9 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -70,6 +73,7 @@ public class InstallScripts { public static final String DEVICE_PROFILE_DIR = "device_profile"; public static final String DEMO_DIR = "demo"; 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 OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates"; public static final String DASHBOARDS_DIR = "dashboards"; @@ -180,6 +184,23 @@ public class InstallScripts { } public void loadSystemWidgets() throws Exception { + Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR); + if (Files.exists(widgetTypesDir)) { + try (DirectoryStream dirStream = Files.newDirectoryStream(widgetTypesDir, path -> path.toString().endsWith(JSON_EXT))) { + dirStream.forEach( + path -> { + try { + JsonNode widgetTypeJson = JacksonUtil.toJsonNode(path.toFile()); + WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); + widgetTypeService.saveWidgetType(widgetTypeDetails); + } catch (Exception e) { + log.error("Unable to load widget type from json: [{}]", path.toString()); + throw new RuntimeException("Unable to load widget type from json", e); + } + } + ); + } + } Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR); try (DirectoryStream dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) { dirStream.forEach( @@ -189,19 +210,29 @@ public class InstallScripts { JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle"); WidgetsBundle widgetsBundle = JacksonUtil.treeToValue(widgetsBundleJson, WidgetsBundle.class); WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); - JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes"); - widgetTypesArrayJson.forEach( - widgetTypeJson -> { - try { - WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); - widgetTypeDetails.setBundleAlias(savedWidgetsBundle.getAlias()); - widgetTypeService.saveWidgetType(widgetTypeDetails); - } catch (Exception e) { - log.error("Unable to load widget type from json: [{}]", path.toString()); - throw new RuntimeException("Unable to load widget type from json", e); + List widgetTypeFqns = new ArrayList<>(); + if (widgetsBundleDescriptorJson.has("widgetTypes")) { + JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes"); + widgetTypesArrayJson.forEach( + widgetTypeJson -> { + try { + WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); + var savedWidgetType = widgetTypeService.saveWidgetType(widgetTypeDetails); + widgetTypeFqns.add(savedWidgetType.getFqn()); + } catch (Exception e) { + log.error("Unable to load widget type from json: [{}]", path.toString()); + throw new RuntimeException("Unable to load widget type from json", e); + } } - } - ); + ); + } + if (widgetsBundleDescriptorJson.has("widgetTypeFqns")) { + JsonNode widgetFqnsArrayJson = widgetsBundleDescriptorJson.get("widgetTypeFqns"); + widgetFqnsArrayJson.forEach(fqnJson -> { + widgetTypeFqns.add(fqnJson.asText()); + }); + } + widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, savedWidgetsBundle.getId(), widgetTypeFqns); } catch (Exception e) { log.error("Unable to load widgets bundle from json: [{}]", path.toString()); throw new RuntimeException("Unable to load widgets bundle from json", e); @@ -268,7 +299,7 @@ public class InstallScripts { } public void loadSystemLwm2mResources() { - Path resourceLwm2mPath = Paths.get(dataDir, MODELS_LWM2M_DIR); + Path resourceLwm2mPath = Paths.get(getDataDir(), MODELS_LWM2M_DIR); try (DirectoryStream dirStream = Files.newDirectoryStream(resourceLwm2mPath, path -> path.toString().endsWith(InstallScripts.XML_EXT))) { dirStream.forEach( path -> { diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 34515f827d..2e2b3e96be 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -756,6 +756,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);"); } catch (Exception e) { } + try { + conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';"); + } catch (Exception e) { + } conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005002;"); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index dafccfdad6..cc9f227b38 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; @@ -205,7 +206,7 @@ public class DefaultDataUpdateService implements DataUpdateService { migrateEdgeEvents("Starting edge events migration. "); break; case "3.5.1": - log.info("Updating data from version 3.5.1 to 3.5.2 ..."); + log.info("Updating data from version 3.5.1 to 3.6.0 ..."); migrateEdgeEvents("Starting edge events migration - adding seq_id column. "); break; default: @@ -502,7 +503,7 @@ public class DefaultDataUpdateService implements DataUpdateService { md.getNodes().add(ruleNode); md.setFirstNodeIndex(newIdx); - md.addConnectionInfo(newIdx, oldIdx, "Success"); + md.addConnectionInfo(newIdx, oldIdx, TbNodeConnectionType.SUCCESS); ruleChainService.saveRuleChainMetaData(tenant.getId(), md, Function.identity()); } } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 1c6af41360..246fa4a0fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.mail; import com.fasterxml.jackson.databind.JsonNode; - import freemarker.template.Configuration; import freemarker.template.Template; import lombok.extern.slf4j.Slf4j; diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java index 0a9b173247..3872fc4c7a 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java @@ -23,18 +23,18 @@ import com.google.api.client.auth.oauth2.TokenResponse; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.lang.Nullable; +import org.springframework.mail.MailException; +import org.springframework.mail.MailSendException; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mail.MailOauth2Provider; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.time.Duration; import java.time.Instant; @@ -50,6 +50,7 @@ public class TbMailSender extends JavaMailSenderImpl { private static final String MAIL_PROP = "mail."; private final TbMailContextComponent ctx; private final Lock lock; + private final Boolean oauth2Enabled; private volatile String accessToken; private volatile long tokenExpires; @@ -70,14 +71,39 @@ public class TbMailSender extends JavaMailSenderImpl { setJavaMailProperties(createJavaMailProperties(jsonConfig)); } - @SneakyThrows + public Boolean getOauth2Enabled() { + return oauth2Enabled; + } + + public long getTokenExpires() { + return tokenExpires; + } + + @Override + protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException { + updateOauth2PasswordIfExpired(); + doSendSuper(mimeMessages, originalMessages); + } + + public void doSendSuper(MimeMessage[] mimeMessages, Object[] originalMessages) { + super.doSend(mimeMessages, originalMessages); + } + @Override - public void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) { - if (oauth2Enabled && (System.currentTimeMillis() > tokenExpires)){ + public void testConnection() throws MessagingException { + updateOauth2PasswordIfExpired(); + testConnectionSuper(); + } + + public void testConnectionSuper() throws MessagingException { + super.testConnection(); + } + + public void updateOauth2PasswordIfExpired() { + if (getOauth2Enabled() && (System.currentTimeMillis() > getTokenExpires())){ refreshAccessToken(); setPassword(accessToken); } - super.doSend(mimeMessages, originalMessages); } private Properties createJavaMailProperties(JsonNode jsonConfig) { @@ -125,10 +151,10 @@ public class TbMailSender extends JavaMailSenderImpl { return javaMailProperties; } - public void refreshAccessToken() throws ThingsboardException { + public void refreshAccessToken() { lock.lock(); try { - if (System.currentTimeMillis() > tokenExpires) { + if (System.currentTimeMillis() > getTokenExpires()) { AdminSettings settings = ctx.getAdminSettingsService().findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); JsonNode jsonValue = settings.getJsonValue(); @@ -151,8 +177,8 @@ public class TbMailSender extends JavaMailSenderImpl { tokenExpires = System.currentTimeMillis() + (tokenResponse.getExpiresInSeconds().intValue() * 1000); } } catch (Exception e) { - log.warn("Unable to retrieve access token: {}", e.getMessage()); - throw new ThingsboardException("Error while retrieving access token: " + e.getMessage(), ThingsboardErrorCode.GENERAL); + log.error("Unable to retrieve access token: {}", e.getMessage()); + throw new RuntimeException("Error while retrieving access token: " + e.getMessage()); } finally { lock.unlock(); } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index cb77cc0948..f6c77691da 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -22,6 +22,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRuleId; @@ -39,9 +40,12 @@ import org.thingsboard.server.common.data.notification.NotificationRequestStatus import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; +import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; @@ -163,16 +167,54 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple .settings(settings) .build(); + processNotificationRequestAsync(ctx, targets, callback); + return request; + } + + @Override + public void sendGeneralWebNotification(TenantId tenantId, UsersFilter recipients, NotificationTemplate template) { + NotificationTarget target = new NotificationTarget(); + target.setTenantId(tenantId); + PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); + targetConfig.setUsersFilter(recipients); + target.setConfiguration(targetConfig); + + NotificationRequest notificationRequest = NotificationRequest.builder() + .tenantId(tenantId) + .template(template) + .targets(List.of(EntityId.NULL_UUID)) // this is temporary and will be removed when 'create from scratch' functionality is implemented for recipients + .status(NotificationRequestStatus.PROCESSING) + .build(); + try { + notificationRequest = notificationRequestService.saveNotificationRequest(tenantId, notificationRequest); + NotificationProcessingContext ctx = NotificationProcessingContext.builder() + .tenantId(tenantId) + .request(notificationRequest) + .deliveryMethods(Set.of(NotificationDeliveryMethod.WEB)) + .template(template) + .build(); + + processNotificationRequestAsync(ctx, List.of(target), null); + } catch (Exception e) { + log.error("Failed to process notification request for recipients {} for template '{}'", recipients, template.getName(), e); + } + } + + private void processNotificationRequestAsync(NotificationProcessingContext ctx, List targets, Consumer callback) { notificationExecutor.submit(() -> { + NotificationRequestId requestId = ctx.getRequest().getId(); for (NotificationTarget target : targets) { - processForTarget(target, ctx); + try { + processForTarget(target, ctx); + } catch (Exception e) { + log.error("[{}] Failed to process notification request for target {}", requestId, target.getId(), e); + } } - - NotificationRequestId requestId = ctx.getRequest().getId(); log.debug("[{}] Notification request processing is finished", requestId); + NotificationRequestStats stats = ctx.getStats(); try { - notificationRequestService.updateNotificationRequest(tenantId, requestId, NotificationRequestStatus.SENT, stats); + notificationRequestService.updateNotificationRequest(ctx.getTenantId(), requestId, NotificationRequestStatus.SENT, stats); } catch (Exception e) { log.error("[{}] Failed to update stats for notification request", requestId, e); } @@ -185,8 +227,6 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple } } }); - - return request; } private void processForTarget(NotificationTarget target, NotificationProcessingContext ctx) { @@ -210,6 +250,11 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple recipients = List.of(targetConfig.getConversation()); break; } + case MICROSOFT_TEAMS: { + MicrosoftTeamsNotificationTargetConfig targetConfig = (MicrosoftTeamsNotificationTargetConfig) target.getConfiguration(); + recipients = List.of(targetConfig); + break; + } default: { recipients = Collections.emptyList(); } @@ -237,7 +282,17 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple private void processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) throws Exception { if (ctx.getStats().contains(deliveryMethod, recipient.getId())) { throw new AlreadySentException(); + } else { + ctx.getStats().reportProcessed(deliveryMethod, recipient.getId()); } + + if (recipient instanceof User) { + UserNotificationSettings settings = notificationSettingsService.getUserNotificationSettings(ctx.getTenantId(), ((User) recipient).getId(), false); + if (!settings.isEnabled(ctx.getNotificationType(), deliveryMethod)) { + throw new RuntimeException("User disabled " + deliveryMethod.getName() + " notifications of this type"); + } + } + NotificationChannel notificationChannel = channels.get(deliveryMethod); DeliveryMethodNotificationTemplate processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient); @@ -251,7 +306,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple Notification notification = Notification.builder() .requestId(request.getId()) .recipientId(recipient.getId()) - .type(ctx.getNotificationTemplate().getNotificationType()) + .type(ctx.getNotificationType()) .subject(processedTemplate.getSubject()) .text(processedTemplate.getBody()) .additionalConfig(processedTemplate.getAdditionalConfig()) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java b/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java index 27a9cabe43..e91a3e80fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java @@ -18,19 +18,18 @@ package org.thingsboard.server.service.notification; import com.google.common.base.Strings; import lombok.Builder; import lombok.Getter; -import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequestStats; +import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; -import org.thingsboard.server.common.data.notification.template.HasSubject; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; -import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.util.TemplateUtils; import java.util.EnumMap; @@ -38,8 +37,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; -import static org.apache.commons.lang3.StringUtils.isNotEmpty; - @SuppressWarnings("unchecked") public class NotificationProcessingContext { @@ -52,6 +49,8 @@ public class NotificationProcessingContext { private final Set deliveryMethods; @Getter private final NotificationTemplate notificationTemplate; + @Getter + private final NotificationType notificationType; private final Map templates; @Getter @@ -65,6 +64,7 @@ public class NotificationProcessingContext { this.deliveryMethods = deliveryMethods; this.settings = settings; this.notificationTemplate = template; + this.notificationType = template.getNotificationType(); this.templates = new EnumMap<>(NotificationDeliveryMethod.class); this.stats = new NotificationRequestStats(); init(); @@ -86,12 +86,11 @@ public class NotificationProcessingContext { public T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) { T template = (T) templates.get(deliveryMethod); - Map additionalTemplateContext = null; if (recipient != null) { - additionalTemplateContext = createTemplateContextForRecipient(recipient); - } - if (MapUtils.isNotEmpty(additionalTemplateContext) && template.containsAny(additionalTemplateContext.keySet().toArray(String[]::new))) { - template = processTemplate(template, additionalTemplateContext); + Map additionalTemplateContext = createTemplateContextForRecipient(recipient); + if (template.getTemplatableValues().stream().anyMatch(value -> value.containsParams(additionalTemplateContext.keySet()))) { + template = processTemplate(template, additionalTemplateContext); + } } return template; } @@ -107,22 +106,13 @@ public class NotificationProcessingContext { if (templateContext.isEmpty()) return template; template = (T) template.copy(); - template.setBody(TemplateUtils.processTemplate(template.getBody(), templateContext)); - if (template instanceof HasSubject) { - String subject = ((HasSubject) template).getSubject(); - ((HasSubject) template).setSubject(TemplateUtils.processTemplate(subject, templateContext)); - } - if (template instanceof WebDeliveryMethodNotificationTemplate) { - WebDeliveryMethodNotificationTemplate webNotificationTemplate = (WebDeliveryMethodNotificationTemplate) template; - String buttonText = webNotificationTemplate.getButtonText(); - if (isNotEmpty(buttonText)) { - webNotificationTemplate.setButtonText(TemplateUtils.processTemplate(buttonText, templateContext)); - } - String buttonLink = webNotificationTemplate.getButtonLink(); - if (isNotEmpty(buttonLink)) { - webNotificationTemplate.setButtonLink(TemplateUtils.processTemplate(buttonLink, templateContext)); + template.getTemplatableValues().forEach(templatableValue -> { + String value = templatableValue.get(); + if (StringUtils.isNotEmpty(value)) { + value = TemplateUtils.processTemplate(value, templateContext); + templatableValue.set(value); } - } + }); return template; } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java new file mode 100644 index 0000000000..8720505267 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java @@ -0,0 +1,194 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.channels; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.Strings; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.info.NotificationInfo; +import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDeliveryMethodNotificationTemplate.Button.LinkType; +import org.thingsboard.server.service.notification.NotificationProcessingContext; +import org.thingsboard.server.service.security.system.SystemSecurityService; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class MicrosoftTeamsNotificationChannel implements NotificationChannel { + + private final SystemSecurityService systemSecurityService; + + @Setter + private RestTemplate restTemplate = new RestTemplateBuilder() + .setConnectTimeout(Duration.of(15, ChronoUnit.SECONDS)) + .setReadTimeout(Duration.of(15, ChronoUnit.SECONDS)) + .build(); + + @Override + public void sendNotification(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception { + Message message = new Message(); + message.setThemeColor(Strings.emptyToNull(processedTemplate.getThemeColor())); + if (StringUtils.isEmpty(processedTemplate.getSubject())) { + message.setText(processedTemplate.getBody()); + } else { + message.setSummary(processedTemplate.getSubject()); + Message.Section section = new Message.Section(); + section.setActivityTitle(processedTemplate.getSubject()); + section.setActivitySubtitle(processedTemplate.getBody()); + message.setSections(List.of(section)); + } + var button = processedTemplate.getButton(); + if (button != null && button.isEnabled()) { + String uri; + if (button.getLinkType() == LinkType.DASHBOARD) { + String state = null; + if (button.isSetEntityIdInState() || StringUtils.isNotEmpty(button.getDashboardState())) { + ObjectNode stateObject = JacksonUtil.newObjectNode(); + if (button.isSetEntityIdInState()) { + stateObject.putObject("params") + .set("entityId", Optional.ofNullable(ctx.getRequest().getInfo()) + .map(NotificationInfo::getStateEntityId) + .map(JacksonUtil::valueToTree) + .orElse(null)); + } else { + stateObject.putObject("params"); + } + if (StringUtils.isNotEmpty(button.getDashboardState())) { + stateObject.put("id", button.getDashboardState()); + } + state = Base64.encodeBase64String(JacksonUtil.OBJECT_MAPPER.writeValueAsBytes(List.of(stateObject))); + } + String baseUrl = systemSecurityService.getBaseUrl(ctx.getTenantId(), null, null); + if (StringUtils.isEmpty(baseUrl)) { + throw new IllegalStateException("Failed to determine base url to construct dashboard link"); + } + uri = baseUrl + "/dashboards/" + button.getDashboardId(); + if (state != null) { + uri += "?state=" + state; + } + } else { + uri = button.getLink(); + } + if (StringUtils.isNotBlank(uri) && button.getText() != null) { + Message.ActionCard actionCard = new Message.ActionCard(); + actionCard.setType("OpenUri"); + actionCard.setName(button.getText()); + var target = new Message.ActionCard.Target("default", uri); + actionCard.setTargets(List.of(target)); + message.setPotentialAction(List.of(actionCard)); + } + } + + restTemplate.postForEntity(targetConfig.getWebhookUrl(), message, String.class); + } + + @Override + public void check(TenantId tenantId) throws Exception { + } + + @Override + public NotificationDeliveryMethod getDeliveryMethod() { + return NotificationDeliveryMethod.MICROSOFT_TEAMS; + } + + @Data + public static class Message { + @JsonProperty("@type") + private final String type = "MessageCard"; + @JsonProperty("@context") + private final String context = "http://schema.org/extensions"; + private String themeColor; + private String summary; + private String text; + private List

sections; + private List potentialAction; + + @Data + public static class Section { + private String activityTitle; + private String activitySubtitle; + private String activityImage; + private List facts; + private boolean markdown; + + @Data + public static class Fact { + private final String name; + private final String value; + } + } + + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class ActionCard { + @JsonProperty("@type") + private String type; // ActionCard, OpenUri + private String name; + private List inputs; // for ActionCard + private List actions; // for ActionCard + private List targets; + + @Data + public static class Input { + @JsonProperty("@type") + private String type; // TextInput, DateInput, MultichoiceInput + private String id; + private boolean isMultiple; + private String title; + private boolean isMultiSelect; + + @Data + public static class Choice { + private final String display; + private final String value; + } + } + + @Data + public static class Action { + @JsonProperty("@type") + private final String type; // HttpPOST + private final String name; + private final String target; // url + } + + @Data + public static class Target { + private final String os; + private final String uri; + } + } + + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 76631aaa95..53ef38d96a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -32,10 +32,10 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasRuleEngineProfile; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.AssetId; @@ -178,15 +178,8 @@ public class DefaultTbClusterService implements TbClusterService { return; } } else { - if (entityId.getEntityType().equals(EntityType.DEVICE)) { - tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceId(entityId.getId()))); - } else if (entityId.getEntityType().equals(EntityType.DEVICE_PROFILE)) { - tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceProfileId(entityId.getId()))); - } else if (entityId.getEntityType().equals(EntityType.ASSET)) { - tbMsg = transformMsg(tbMsg, assetProfileCache.get(tenantId, new AssetId(entityId.getId()))); - } else if (entityId.getEntityType().equals(EntityType.ASSET_PROFILE)) { - tbMsg = transformMsg(tbMsg, assetProfileCache.get(tenantId, new AssetProfileId(entityId.getId()))); - } + HasRuleEngineProfile ruleEngineProfile = getRuleEngineProfileForEntityOrElseNull(tenantId, entityId); + tbMsg = transformMsg(tbMsg, ruleEngineProfile); } TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, entityId); log.trace("PUSHING msg: {} to:{}", tbMsg, tpi); @@ -198,34 +191,33 @@ public class DefaultTbClusterService implements TbClusterService { toRuleEngineMsgs.incrementAndGet(); } - private TbMsg transformMsg(TbMsg tbMsg, DeviceProfile deviceProfile) { - if (deviceProfile != null) { - RuleChainId targetRuleChainId = deviceProfile.getDefaultRuleChainId(); - String targetQueueName = deviceProfile.getDefaultQueueName(); - tbMsg = transformMsg(tbMsg, targetRuleChainId, targetQueueName); + private HasRuleEngineProfile getRuleEngineProfileForEntityOrElseNull(TenantId tenantId, EntityId entityId) { + if (entityId.getEntityType().equals(EntityType.DEVICE)) { + return deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); + } else if (entityId.getEntityType().equals(EntityType.DEVICE_PROFILE)) { + return deviceProfileCache.get(tenantId, new DeviceProfileId(entityId.getId())); + } else if (entityId.getEntityType().equals(EntityType.ASSET)) { + return assetProfileCache.get(tenantId, new AssetId(entityId.getId())); + } else if (entityId.getEntityType().equals(EntityType.ASSET_PROFILE)) { + return assetProfileCache.get(tenantId, new AssetProfileId(entityId.getId())); } - return tbMsg; - } - - private TbMsg transformMsg(TbMsg tbMsg, AssetProfile assetProfile) { - if (assetProfile != null) { - RuleChainId targetRuleChainId = assetProfile.getDefaultRuleChainId(); - String targetQueueName = assetProfile.getDefaultQueueName(); - tbMsg = transformMsg(tbMsg, targetRuleChainId, targetQueueName); - } - return tbMsg; - } - - private TbMsg transformMsg(TbMsg tbMsg, RuleChainId targetRuleChainId, String targetQueueName) { - boolean isRuleChainTransform = targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId()); - boolean isQueueTransform = targetQueueName != null && !targetQueueName.equals(tbMsg.getQueueName()); - - if (isRuleChainTransform && isQueueTransform) { - tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId, targetQueueName); - } else if (isRuleChainTransform) { - tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId); - } else if (isQueueTransform) { - tbMsg = TbMsg.transformMsg(tbMsg, targetQueueName); + return null; + } + private TbMsg transformMsg(TbMsg tbMsg, HasRuleEngineProfile ruleEngineProfile) { + if (ruleEngineProfile != null) { + RuleChainId targetRuleChainId = ruleEngineProfile.getDefaultRuleChainId(); + String targetQueueName = ruleEngineProfile.getDefaultQueueName(); + + boolean isRuleChainTransform = targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId()); + boolean isQueueTransform = targetQueueName != null && !targetQueueName.equals(tbMsg.getQueueName()); + + if (isRuleChainTransform && isQueueTransform) { + tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId, targetQueueName); + } else if (isRuleChainTransform) { + tbMsg = TbMsg.transformMsgRuleChainId(tbMsg, targetRuleChainId); + } else if (isQueueTransform) { + tbMsg = TbMsg.transformMsgQueueName(tbMsg, targetQueueName); + } } return tbMsg; } @@ -466,11 +458,6 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void onDeviceUpdated(Device device, Device old) { - onDeviceUpdated(device, old, true); - } - - @Override - public void onDeviceUpdated(Device device, Device old, boolean notifyEdge) { var created = old == null; broadcastEntityChangeToTransport(device.getTenantId(), device.getId(), device, null); if (old != null) { @@ -485,9 +472,6 @@ public class DefaultTbClusterService implements TbClusterService { broadcastEntityStateChangeEvent(device.getTenantId(), device.getId(), created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); sendDeviceStateServiceEvent(device.getTenantId(), device.getId(), created, !created, false); otaPackageStateService.update(device, old); - if (!created && notifyEdge) { - sendNotificationMsgToEdge(device.getTenantId(), null, device.getId(), null, null, EdgeEventActionType.UPDATED); - } } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index dc24d6df33..fef22acf40 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -32,10 +32,11 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; -import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; @@ -63,7 +64,6 @@ import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.DataDecodingEncodingService; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index 51f4d5283f..670dbcaa6d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -23,11 +23,14 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.gen.MsgProtos; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleNodeInfo; @@ -42,12 +45,14 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbRuleEngineComponent; @@ -98,6 +103,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< private boolean statsEnabled; @Value("${queue.rule-engine.prometheus-stats.enabled:false}") boolean prometheusStatsEnabled; + @Value("${queue.rule-engine.topic-deletion-delay:30}") + private int topicDeletionDelayInSec; private final StatsFactory statsFactory; private final TbRuleEngineSubmitStrategyFactory submitStrategyFactory; @@ -107,7 +114,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< private final TbRuleEngineDeviceRpcService tbDeviceRpcService; private final TbServiceInfoProvider serviceInfoProvider; private final QueueService queueService; - // private final TenantId tenantId; + private final TbQueueProducerProvider producerProvider; + private final TbQueueAdmin queueAdmin; private final ConcurrentMap>> consumers = new ConcurrentHashMap<>(); private final ConcurrentMap consumerConfigurations = new ConcurrentHashMap<>(); private final ConcurrentMap consumerStats = new ConcurrentHashMap<>(); @@ -128,7 +136,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< TbTenantProfileCache tenantProfileCache, TbApiUsageStateService apiUsageStateService, PartitionService partitionService, ApplicationEventPublisher eventPublisher, - TbServiceInfoProvider serviceInfoProvider, QueueService queueService) { + TbServiceInfoProvider serviceInfoProvider, QueueService queueService, + TbQueueProducerProvider producerProvider, TbQueueAdmin queueAdmin) { super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), Optional.empty()); this.statisticsService = statisticsService; this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory; @@ -138,6 +147,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< this.statsFactory = statsFactory; this.serviceInfoProvider = serviceInfoProvider; this.queueService = queueService; + this.producerProvider = producerProvider; + this.queueAdmin = queueAdmin; } @PostConstruct @@ -145,14 +156,16 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< super.init("tb-rule-engine-consumer", "tb-rule-engine-notifications-consumer"); List queues = queueService.findAllQueues(); for (Queue configuration : queues) { - initConsumer(configuration); + if (partitionService.isManagedByCurrentService(configuration.getTenantId())) { + initConsumer(configuration); + } } } private void initConsumer(Queue configuration) { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, configuration); consumerConfigurations.putIfAbsent(queueKey, configuration); - consumerStats.putIfAbsent(queueKey, new TbRuleEngineConsumerStats(configuration.getName(), statsFactory)); + consumerStats.putIfAbsent(queueKey, new TbRuleEngineConsumerStats(configuration, statsFactory)); if (!configuration.isConsumerPerPartition()) { consumers.computeIfAbsent(queueKey, queueName -> tbRuleEngineQueueFactory.createToRuleEngineMsgConsumer(configuration)); } else { @@ -172,7 +185,12 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< if (event.getServiceType().equals(getServiceType())) { String serviceQueue = event.getQueueKey().getQueueName(); log.info("[{}] Subscribing to partitions: {}", serviceQueue, event.getPartitions()); - if (!consumerConfigurations.get(event.getQueueKey()).isConsumerPerPartition()) { + Queue configuration = consumerConfigurations.get(event.getQueueKey()); + if (configuration == null) { + log.warn("Received invalid partition change event for {} that is not managed by this service", event.getQueueKey()); + return; + } + if (!configuration.isConsumerPerPartition()) { consumers.get(event.getQueueKey()).subscribe(event.getPartitions()); } else { log.info("[{}] Subscribing consumer per partition: {}", serviceQueue, event.getPartitions()); @@ -230,7 +248,6 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< launchConsumer(consumer, consumerConfigurations.get(queueKey), consumerStats.get(queueKey), "" + queueKey + "-" + tpi.getPartition().orElse(-999999)); consumer.subscribe(Collections.singleton(tpi)); }); - } finally { tbTopicWithConsumerPerPartition.getLock().unlock(); } @@ -278,9 +295,9 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< void consumerLoop(TbQueueConsumer> consumer, org.thingsboard.server.common.data.queue.Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) { updateCurrentThreadName(threadSuffix); - while (!stopped && !consumer.isStopped()) { + while (!stopped && !consumer.isStopped() && !consumer.isQueueDeleted()) { try { - List> msgs = consumer.poll(pollDuration); + List> msgs = consumer.poll(configuration.getPollInterval()); if (msgs.isEmpty()) { continue; } @@ -328,6 +345,10 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< } } } + + if (consumer.isQueueDeleted()) { + processQueueDeletion(configuration, consumer); + } log.info("TB Rule Engine Consumer stopped."); } @@ -363,7 +384,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< callback.onSuccess(); } } catch (Exception e) { - callback.onFailure(new RuleEngineException(e.getMessage())); + callback.onFailure(new RuleEngineException(e.getMessage(), e)); } } @@ -425,32 +446,34 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< private void updateQueue(TransportProtos.QueueUpdateMsg queueUpdateMsg) { log.info("Received queue update msg: [{}]", queueUpdateMsg); - String queueName = queueUpdateMsg.getQueueName(); TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); - QueueId queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())); - QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName(), tenantId); - Queue queue = queueService.findQueueById(tenantId, queueId); - Queue oldQueue = consumerConfigurations.remove(queueKey); - if (oldQueue != null) { - if (oldQueue.isConsumerPerPartition()) { - TbTopicWithConsumerPerPartition consumerPerPartition = topicsConsumerPerPartition.remove(queueKey); - ReentrantLock lock = consumerPerPartition.getLock(); - try { - lock.lock(); - consumerPerPartition.getConsumers().values().forEach(TbQueueConsumer::unsubscribe); - } finally { - lock.unlock(); + if (partitionService.isManagedByCurrentService(tenantId)) { + QueueId queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())); + String queueName = queueUpdateMsg.getQueueName(); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueName, tenantId); + Queue queue = queueService.findQueueById(tenantId, queueId); + Queue oldQueue = consumerConfigurations.remove(queueKey); + if (oldQueue != null) { + if (oldQueue.isConsumerPerPartition()) { + TbTopicWithConsumerPerPartition consumerPerPartition = topicsConsumerPerPartition.remove(queueKey); + ReentrantLock lock = consumerPerPartition.getLock(); + try { + lock.lock(); + consumerPerPartition.getConsumers().values().forEach(TbQueueConsumer::unsubscribe); + } finally { + lock.unlock(); + } + } else { + TbQueueConsumer> consumer = consumers.remove(queueKey); + consumer.unsubscribe(); } - } else { - TbQueueConsumer> consumer = consumers.remove(queueKey); - consumer.unsubscribe(); } - } - initConsumer(queue); + initConsumer(queue); - if (!queue.isConsumerPerPartition()) { - launchConsumer(consumers.get(queueKey), consumerConfigurations.get(queueKey), consumerStats.get(queueKey), queueName); + if (!queue.isConsumerPerPartition()) { + launchConsumer(consumers.get(queueKey), consumerConfigurations.get(queueKey), consumerStats.get(queueKey), queueName); + } } partitionService.updateQueue(queueUpdateMsg); @@ -462,22 +485,22 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); + partitionService.removeQueue(queueDeleteMsg); Queue queue = consumerConfigurations.remove(queueKey); if (queue != null) { if (queue.isConsumerPerPartition()) { TbTopicWithConsumerPerPartition tbTopicWithConsumerPerPartition = topicsConsumerPerPartition.remove(queueKey); if (tbTopicWithConsumerPerPartition != null) { - tbTopicWithConsumerPerPartition.getConsumers().values().forEach(TbQueueConsumer::unsubscribe); + tbTopicWithConsumerPerPartition.getConsumers().values().forEach(TbQueueConsumer::onQueueDelete); tbTopicWithConsumerPerPartition.getConsumers().clear(); } } else { TbQueueConsumer> consumer = consumers.remove(queueKey); if (consumer != null) { - consumer.unsubscribe(); + consumer.onQueueDelete(); } } } - partitionService.removeQueue(queueDeleteMsg); } private void forwardToRuleEngineActor(String queueName, TenantId tenantId, ToRuleEngineMsg toRuleEngineMsg, TbMsgCallback callback) { @@ -496,6 +519,47 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< actorContext.tell(msg); } + private void processQueueDeletion(Queue queue, TbQueueConsumer> consumer) { + long finishTs = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(topicDeletionDelayInSec); + try { + int n = 0; + while (System.currentTimeMillis() <= finishTs) { + List> msgs = consumer.poll(queue.getPollInterval()); + if (msgs.isEmpty()) { + continue; + } + for (TbProtoQueueMsg msg : msgs) { + try { + MsgProtos.TbMsgProto tbMsgProto = MsgProtos.TbMsgProto.parseFrom(msg.getValue().getTbMsg().toByteArray()); + EntityId originator = EntityIdFactory.getByTypeAndUuid(tbMsgProto.getEntityType(), new UUID(tbMsgProto.getEntityIdMSB(), tbMsgProto.getEntityIdLSB())); + + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, queue.getName(), TenantId.SYS_TENANT_ID, originator); + producerProvider.getRuleEngineMsgProducer().send(tpi, msg, null); + n++; + } catch (Throwable e) { + log.debug("Failed to move message to system {}: {}", consumer.getTopic(), msg, e); + } + } + consumer.commit(); + } + if (n > 0) { + log.info("Moved {} messages from {} to system {}", n, consumer.getFullTopicNames(), consumer.getTopic()); + } + + consumer.unsubscribe(); + for (String topic : consumer.getFullTopicNames()) { + try { + queueAdmin.deleteTopic(topic); + log.info("Deleted topic {}", topic); + } catch (Exception e) { + log.error("Failed to delete topic {} after unsubscribing", topic, e); + } + } + } catch (Exception e) { + log.error("Failed to process deletion of {} ({})", consumer.getTopic(), queue.getTenantId(), e); + } + } + @Scheduled(fixedDelayString = "${queue.rule-engine.stats.print-interval-ms}") public void printStats() { if (statsEnabled) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java index 400586235e..ea90364ef7 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java @@ -22,7 +22,6 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.exception.TenantNotFoundException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.discovery.TenantRoutingInfo; import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; @@ -31,12 +30,9 @@ import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; @ConditionalOnExpression("'${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core' || '${service.type:null}'=='tb-rule-engine'") public class DefaultTenantRoutingInfoService implements TenantRoutingInfoService { - private final TenantService tenantService; - private final TbTenantProfileCache tenantProfileCache; - public DefaultTenantRoutingInfoService(TenantService tenantService, TbTenantProfileCache tenantProfileCache) { - this.tenantService = tenantService; + public DefaultTenantRoutingInfoService(TbTenantProfileCache tenantProfileCache) { this.tenantProfileCache = tenantProfileCache; } @@ -44,7 +40,7 @@ public class DefaultTenantRoutingInfoService implements TenantRoutingInfoService public TenantRoutingInfo getRoutingInfo(TenantId tenantId) { TenantProfile tenantProfile = tenantProfileCache.get(tenantId); if (tenantProfile != null) { - return new TenantRoutingInfo(tenantId, tenantProfile.isIsolatedTbRuleEngine()); + return new TenantRoutingInfo(tenantId, tenantProfile.getId(), tenantProfile.isIsolatedTbRuleEngine()); } else { throw new TenantNotFoundException(tenantId); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java index ef2ba8798d..c2171c2550 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java @@ -17,11 +17,14 @@ package org.thingsboard.server.service.queue; import io.micrometer.core.instrument.Timer; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.ExceptionUtil; +import org.thingsboard.server.common.data.exception.AbstractRateLimitException; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.TbMsgCallback; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -57,8 +60,23 @@ public class TbMsgPackCallback implements TbMsgCallback { ctx.onSuccess(id); } + @Override + public void onRateLimit(RuleEngineException e) { + log.debug("[{}] ON RATE LIMIT", id, e); + //TODO notify tenant on rate limit + if (failedMsgTimer != null) { + failedMsgTimer.record(System.currentTimeMillis() - startMsgProcessing, TimeUnit.MILLISECONDS); + } + ctx.onSuccess(id); + } + @Override public void onFailure(RuleEngineException e) { + if (ExceptionUtil.lookupExceptionInCause(e, AbstractRateLimitException.class) != null) { + onRateLimit(e); + return; + } + log.trace("[{}] ON FAILURE", id, e); if (failedMsgTimer != null) { failedMsgTimer.record(System.currentTimeMillis() - startMsgProcessing, TimeUnit.MILLISECONDS); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbRuleEngineConsumerStats.java b/application/src/main/java/org/thingsboard/server/service/queue/TbRuleEngineConsumerStats.java index 77e805eb62..2904c299ce 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbRuleEngineConsumerStats.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbRuleEngineConsumerStats.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.queue; import io.micrometer.core.instrument.Timer; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; @@ -63,9 +64,11 @@ public class TbRuleEngineConsumerStats { private final ConcurrentMap tenantExceptions = new ConcurrentHashMap<>(); private final String queueName; + private final TenantId tenantId; - public TbRuleEngineConsumerStats(String queueName, StatsFactory statsFactory) { - this.queueName = queueName; + public TbRuleEngineConsumerStats(Queue queue, StatsFactory statsFactory) { + this.queueName = queue.getName(); + this.tenantId = queue.getTenantId(); this.statsFactory = statsFactory; String statsKey = StatsType.RULE_ENGINE.getName() + "." + queueName; @@ -156,7 +159,11 @@ public class TbRuleEngineConsumerStats { counters.forEach(counter -> { stats.append(counter.getName()).append(" = [").append(counter.get()).append("] "); }); - log.info("[{}] Stats: {}", queueName, stats); + if (tenantId.isSysTenantId()) { + log.info("[{}] Stats: {}", queueName, stats); + } else { + log.info("[{}][{}] Stats: {}", queueName, tenantId, stats); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java index bca7c7d81a..8ea7208c55 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.rpc; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -27,6 +26,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -182,7 +182,7 @@ public class DefaultTbCoreDeviceRpcService implements TbCoreDeviceRpcService { entityNode.put(DataConstants.ADDITIONAL_INFO, msg.getAdditionalInfo()); try { - TbMsg tbMsg = TbMsg.newMsg(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE, msg.getDeviceId(), Optional.ofNullable(currentUser).map(User::getCustomerId).orElse(null), metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, msg.getDeviceId(), Optional.ofNullable(currentUser).map(User::getCustomerId).orElse(null), metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); clusterService.pushMsgToRuleEngine(msg.getTenantId(), msg.getDeviceId(), tbMsg, null); } catch (IllegalArgumentException e) { throw new RuntimeException(e); diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java index ad40176cef..8c6f0d768c 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java @@ -24,6 +24,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rpc.Rpc; @@ -62,7 +63,7 @@ public class TbRpcService { } private void pushRpcMsgToRuleEngine(TenantId tenantId, Rpc rpc) { - TbMsg msg = TbMsg.newMsg("RPC_" + rpc.getStatus().name(), rpc.getDeviceId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(rpc)); + TbMsg msg = TbMsg.newMsg(TbMsgType.valueOf("RPC_" + rpc.getStatus().name()), rpc.getDeviceId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(rpc)); tbClusterService.pushMsgToRuleEngine(tenantId, rpc.getDeviceId(), msg, null); } diff --git a/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java b/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java index 5756101ee1..884c614231 100644 --- a/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java +++ b/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java @@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -185,9 +184,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedRuleChain.getId(), actionType.equals(ActionType.ADDED) ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); } - boolean sendMsgToEdge = RuleChainType.EDGE.equals(savedRuleChain.getType()) && actionType.equals(ActionType.UPDATED); - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, savedRuleChain.getId(), - savedRuleChain, user, actionType, sendMsgToEdge, null); + notificationEntityService.logEntityAction(tenantId, savedRuleChain.getId(), savedRuleChain, null, actionType, user); return savedRuleChain; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.RULE_CHAIN), ruleChain, actionType, user, e); @@ -204,11 +201,6 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement Set referencingRuleChainIds = referencingRuleNodes.stream().map(RuleNode::getRuleChainId).collect(Collectors.toSet()); - List relatedEdgeIds = null; - if (RuleChainType.EDGE.equals(ruleChain.getType())) { - relatedEdgeIds = edgeService.findAllRelatedEdgeIds(tenantId, ruleChainId); - } - ruleChainService.deleteRuleChainById(tenantId, ruleChainId); referencingRuleChainIds.remove(ruleChain.getId()); @@ -220,7 +212,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement tbClusterService.broadcastEntityStateChangeEvent(tenantId, ruleChain.getId(), ComponentLifecycleEvent.DELETED); } - notificationEntityService.notifyDeleteRuleChain(tenantId, ruleChain, relatedEdgeIds, user); + notificationEntityService.logEntityAction(tenantId, ruleChainId, ruleChain, null, ActionType.DELETED, user, ruleChainId.toString()); } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.RULE_CHAIN), ActionType.DELETED, user, e, ruleChainId.toString()); @@ -310,14 +302,8 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement notificationEntityService.logEntityAction(tenantId, ruleChainId, ruleChain, ActionType.UPDATED, user, ruleChainMetaData); - if (RuleChainType.EDGE.equals(ruleChain.getType())) { - notificationEntityService.notifySendMsgToEdgeService(tenantId, ruleChain.getId(), EdgeEventActionType.UPDATED); - } - for (RuleChain updatedRuleChain : updatedRuleChains) { - if (RuleChainType.EDGE.equals(ruleChain.getType())) { - notificationEntityService.notifySendMsgToEdgeService(tenantId, updatedRuleChain.getId(), EdgeEventActionType.UPDATED); - } else { + if (RuleChainType.CORE.equals(ruleChain.getType())) { RuleChainMetaData updatedRuleChainMetaData = checkNotNull(ruleChainService.loadRuleChainMetaData(tenantId, updatedRuleChain.getId())); notificationEntityService.logEntityAction(tenantId, updatedRuleChain.getId(), updatedRuleChain, ActionType.UPDATED, user, updatedRuleChainMetaData); @@ -333,34 +319,34 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement @Override public RuleChain assignRuleChainToEdge(TenantId tenantId, RuleChain ruleChain, Edge edge, User user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_EDGE; RuleChainId ruleChainId = ruleChain.getId(); EdgeId edgeId = edge.getId(); try { RuleChain savedRuleChain = checkNotNull(ruleChainService.assignRuleChainToEdge(tenantId, ruleChainId, edgeId)); - notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, ruleChainId, - null, edgeId, savedRuleChain, ActionType.ASSIGNED_TO_EDGE, + notificationEntityService.logEntityAction(tenantId, ruleChainId, savedRuleChain, null, actionType, user, ruleChainId.toString(), edgeId.toString(), edge.getName()); return savedRuleChain; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.RULE_CHAIN), - ActionType.ASSIGNED_TO_EDGE, user, e, ruleChainId.toString(), edgeId.toString()); + actionType, user, e, ruleChainId.toString(), edgeId.toString()); throw e; } } @Override public RuleChain unassignRuleChainFromEdge(TenantId tenantId, RuleChain ruleChain, Edge edge, User user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_EDGE; RuleChainId ruleChainId = ruleChain.getId(); EdgeId edgeId = edge.getId(); try { RuleChain savedRuleChain = checkNotNull(ruleChainService.unassignRuleChainFromEdge(tenantId, ruleChainId, edgeId, false)); - notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, ruleChainId, - null, edgeId, savedRuleChain, ActionType.UNASSIGNED_FROM_EDGE, + notificationEntityService.logEntityAction(tenantId, ruleChainId, savedRuleChain, null, actionType, user, ruleChainId.toString(), edgeId.toString(), edge.getName()); return savedRuleChain; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.RULE_CHAIN), - ActionType.UNASSIGNED_FROM_EDGE, user, e, ruleChainId, edgeId); + actionType, user, e, ruleChainId, edgeId); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index d159c469c0..d350a86f91 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -22,11 +22,14 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.security.model.JwtSettings; +import org.thingsboard.server.dao.notification.DefaultNotifications; import org.thingsboard.server.dao.settings.AdminSettingsService; import java.nio.charset.StandardCharsets; @@ -43,6 +46,7 @@ public class DefaultJwtSettingsService implements JwtSettingsService { private final AdminSettingsService adminSettingsService; @Lazy private final Optional tbClusterService; + private final Optional notificationCenter; private final JwtSettingsValidator jwtSettingsValidator; @Value("${security.jwt.tokenExpirationTime:9000}") @@ -124,6 +128,9 @@ public class DefaultJwtSettingsService implements JwtSettingsService { log.warn("WARNING: The platform is configured to use default JWT Signing Key. " + "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " + "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator."); + notificationCenter.ifPresent(notificationCenter -> { + notificationCenter.sendGeneralWebNotification(TenantId.SYS_TENANT_ID, new SystemAdministratorsFilter(), DefaultNotifications.jwtSigningKeyIssue.toTemplate()); + }); } this.jwtSettings = result; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 726a5d98f0..2c2956b288 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -55,7 +55,6 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserServiceImpl; -import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; import org.thingsboard.server.service.security.model.SecurityUser; @@ -73,7 +72,6 @@ import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTING @Service @Slf4j -@TbCoreComponent public class DefaultSystemSecurityService implements SystemSecurityService { @Autowired @@ -249,11 +247,11 @@ public class DefaultSystemSecurityService implements SystemSecurityService { JsonNode prohibitDifferentUrl = generalSettings.getJsonValue().get("prohibitDifferentUrl"); - if (prohibitDifferentUrl != null && prohibitDifferentUrl.asBoolean()) { + if ((prohibitDifferentUrl != null && prohibitDifferentUrl.asBoolean()) || httpServletRequest == null) { baseUrl = generalSettings.getJsonValue().get("baseUrl").asText(); } - if (StringUtils.isEmpty(baseUrl)) { + if (StringUtils.isEmpty(baseUrl) && httpServletRequest != null) { baseUrl = MiscUtils.constructBaseUrl(httpServletRequest); } diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 554359fd6d..cdc5afe361 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -36,7 +36,6 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageRecordKey; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.EntityType; @@ -51,6 +50,8 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.query.EntityData; @@ -63,7 +64,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; -import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -102,10 +102,7 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.DataConstants.ACTIVITY_EVENT; -import static org.thingsboard.server.common.data.DataConstants.CONNECT_EVENT; -import static org.thingsboard.server.common.data.DataConstants.DISCONNECT_EVENT; -import static org.thingsboard.server.common.data.DataConstants.INACTIVITY_EVENT; +import static org.thingsboard.server.common.data.DataConstants.SCOPE; import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; /** @@ -229,9 +226,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService= deviceState.getLastActivityTime()) { + deviceState.setLastInactivityAlarmTime(0L); + save(deviceId, INACTIVITY_ALARM_TIME, 0L); + } } } } @@ -533,7 +534,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService> tsData = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), PERSISTENT_ATTRIBUTES); future = Futures.transform(tsData, extractDeviceStateData(device), deviceStateExecutor); } else { - ListenableFuture> attrData = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.SERVER_SCOPE, PERSISTENT_ATTRIBUTES); + ListenableFuture> attrData = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), SERVER_SCOPE, PERSISTENT_ATTRIBUTES); future = Futures.transform(attrData, extractDeviceStateData(device), deviceStateExecutor); } return transformInactivityTimeout(future); @@ -771,11 +772,11 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService(deviceId, key, value)); } else { - tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new TelemetrySaveCallback<>(deviceId, key, value)); + tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, SERVER_SCOPE, key, value, new TelemetrySaveCallback<>(deviceId, key, value)); } } @@ -811,7 +812,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService(deviceId, key, value)); } else { - tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new TelemetrySaveCallback<>(deviceId, key, value)); + tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, SERVER_SCOPE, key, value, new TelemetrySaveCallback<>(deviceId, key, value)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 3b37b0fe26..1cf0f0fc4d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -25,15 +25,15 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.util.ThrowingRunnable; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.TbNotificationEntityService; import org.thingsboard.server.service.sync.ie.exporting.EntityExportService; import org.thingsboard.server.service.sync.ie.exporting.impl.BaseEntityExportService; @@ -65,9 +65,10 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS private final TbNotificationEntityService entityNotificationService; protected static final List SUPPORTED_ENTITY_TYPES = List.of( - EntityType.CUSTOMER, EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.RULE_CHAIN, - EntityType.DASHBOARD, EntityType.DEVICE_PROFILE, EntityType.DEVICE, - EntityType.ENTITY_VIEW, EntityType.WIDGETS_BUNDLE, + EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.DASHBOARD, + EntityType.ASSET_PROFILE, EntityType.ASSET, + EntityType.DEVICE_PROFILE, EntityType.DEVICE, + EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE ); @@ -119,8 +120,8 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS relationService.saveRelations(ctx.getTenantId(), new ArrayList<>(ctx.getRelations())); for (EntityRelation relation : ctx.getRelations()) { - entityNotificationService.notifyRelation(ctx.getTenantId(), null, - relation, ctx.getUser(), ActionType.RELATION_ADD_OR_UPDATE, relation); + entityNotificationService.logEntityRelationAction(ctx.getTenantId(), null, + relation, ctx.getUser(), ActionType.RELATION_ADD_OR_UPDATE, null, relation); } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java index 09b96d68fa..abc90221be 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java @@ -26,6 +26,7 @@ import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; import java.util.Collection; import java.util.Set; +import java.util.regex.Pattern; import java.util.UUID; import java.util.function.Function; import java.util.stream.Stream; @@ -47,8 +48,8 @@ public abstract class BaseEntityExportService getSupportedEntityTypes(); - protected void replaceUuidsRecursively(EntitiesExportCtx ctx, JsonNode node, Set skipFieldsSet) { - JacksonUtil.replaceUuidsRecursively(node, skipFieldsSet, uuid -> getExternalIdOrElseInternalByUuid(ctx, uuid)); + protected void replaceUuidsRecursively(EntitiesExportCtx ctx, JsonNode node, Set skipFieldsSet, Pattern includedFieldsPattern) { + JacksonUtil.replaceUuidsRecursively(node, skipFieldsSet, includedFieldsPattern, uuid -> getExternalIdOrElseInternalByUuid(ctx, uuid)); } protected Stream toExternalIds(Collection internalIds, Function entityIdCreator, diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java index b408aadd68..0df1e83c7f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java @@ -26,7 +26,6 @@ import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; -import java.util.Collections; import java.util.Set; @Service @@ -41,10 +40,10 @@ public class DashboardExportService extends BaseEntityExportService { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java index 7024f36623..3fe9cd9e4a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java @@ -42,7 +42,7 @@ public class WidgetsBundleExportService extends BaseEntityExportService widgets = widgetTypeService.findWidgetTypesDetailsByTenantIdAndBundleAlias(ctx.getTenantId(), widgetsBundle.getAlias()); + List widgets = widgetTypeService.findWidgetFqnsByWidgetsBundleId(ctx.getTenantId(), widgetsBundle.getId()); exportData.setWidgets(widgets); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java index 7f0af64ed7..3255c418c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java @@ -58,8 +58,8 @@ public class AssetProfileImportService extends BaseEntityImportService { - entityNotificationService.notifyRelation(tenantId, null, - existingRelation, ctx.getUser(), ActionType.RELATION_DELETED, existingRelation); + entityNotificationService.logEntityRelationAction(tenantId, null, + existingRelation, ctx.getUser(), ActionType.RELATION_DELETED, null, existingRelation); }); } else if (Objects.equal(relation.getAdditionalInfo(), existingRelation.getAdditionalInfo())) { relationsMap.remove(relation); @@ -266,8 +267,8 @@ public abstract class BaseEntityImportService skipFieldsSet, LinkedHashSet hints) { - JacksonUtil.replaceUuidsRecursively(entityAlias, skipFieldsSet, - uuid -> idProvider.getInternalIdByUuid(uuid, ctx.isFinalImportAttempt(), hints).map(EntityId::getId).orElse(uuid)); + protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode json, + Set skipFieldsSet, Pattern includedFieldsPattern, + LinkedHashSet hints) { + JacksonUtil.replaceUuidsRecursively(json, skipFieldsSet, includedFieldsPattern, + uuid -> idProvider.getInternalIdByUuid(uuid, ctx.isFinalImportAttempt(), hints) + .map(EntityId::getId).orElse(uuid)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java index 250026c71c..eac229f774 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java @@ -65,10 +65,10 @@ public class DashboardImportService extends BaseEntityImportService exportData, IdProvider idProvider) { for (JsonNode entityAlias : dashboard.getEntityAliasesConfig()) { - replaceIdsRecursively(ctx, idProvider, entityAlias, Set.of("id"), HINTS); + replaceIdsRecursively(ctx, idProvider, entityAlias, Set.of("id"), null, HINTS); } for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) { - replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Set.of("id"), HINTS); + replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Set.of("id"), null, HINTS); } return dashboard; } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java index 3cc0310079..70d09f6678 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java @@ -68,8 +68,8 @@ public class DeviceProfileImportService extends BaseEntityImportService { private static final LinkedHashSet HINTS = new LinkedHashSet<>(Arrays.asList(EntityType.RULE_CHAIN, EntityType.DEVICE, EntityType.ASSET)); + public static final Pattern PROCESSED_CONFIG_FIELDS_PATTERN = Pattern.compile(".*[iI]d.*"); private final TbRuleChainService tbRuleChainService; private final RuleChainService ruleChainService; @@ -92,7 +93,8 @@ public class RuleChainImportService extends BaseEntityImportService replaceIdsRecursively(ctx, idProvider, ruleNode.getConfiguration(), Collections.emptySet(), HINTS)); + ruleNodes.forEach(ruleNode -> replaceIdsRecursively(ctx, idProvider, ruleNode.getConfiguration(), + Collections.emptySet(), PROCESSED_CONFIG_FIELDS_PATTERN, HINTS)); Optional.ofNullable(metaData.getRuleChainConnections()).orElse(Collections.emptyList()) .forEach(ruleChainConnectionInfo -> { ruleChainConnectionInfo.setTargetRuleChainId(idProvider.getInternalId(ruleChainConnectionInfo.getTargetRuleChainId(), false)); @@ -128,14 +130,12 @@ public class RuleChainImportService extends BaseEntityImportService existingWidgets = widgetTypeService.findWidgetTypesInfosByTenantIdAndBundleAlias(ctx.getTenantId(), savedWidgetsBundle.getAlias()).stream() - .collect(Collectors.toMap(BaseWidgetType::getAlias, w -> w)); - for (WidgetTypeDetails widget : exportData.getWidgets()) { - WidgetTypeInfo existingWidget; - if ((existingWidget = existingWidgets.remove(widget.getAlias())) != null) { - widget.setId(existingWidget.getId()); - widget.setCreatedTime(existingWidget.getCreatedTime()); - } else { - widget.setId(null); - } - widget.setTenantId(ctx.getTenantId()); - widget.setBundleAlias(savedWidgetsBundle.getAlias()); - widgetTypeService.saveWidgetType(widget); - } - existingWidgets.values().stream() - .map(BaseWidgetType::getId) - .forEach(widgetTypeId -> widgetTypeService.deleteWidgetType(ctx.getTenantId(), widgetTypeId)); - } + widgetTypeService.updateWidgetsBundleWidgetFqns(ctx.getTenantId(), savedWidgetsBundle.getId(), exportData.getWidgets()); return savedWidgetsBundle; } @@ -91,12 +63,6 @@ public class WidgetsBundleImportService extends BaseEntityImportService taskCache; @@ -432,12 +429,11 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont return exportableEntitiesService.findEntitiesByTenantId(ctx.getTenantId(), entityType, pageLink); }, 100, entity -> { if (ctx.getImportedEntities().get(entityType) == null || !ctx.getImportedEntities().get(entityType).contains(entity.getId())) { - List relatedEdgeIds = edgeService.findAllRelatedEdgeIds(ctx.getTenantId(), entity.getId()); exportableEntitiesService.removeById(ctx.getTenantId(), entity.getId()); ctx.addEventCallback(() -> { - entityNotificationService.notifyDeleteEntity(ctx.getTenantId(), entity.getId(), - entity, null, ActionType.DELETED, relatedEdgeIds, ctx.getUser()); + entityNotificationService.logEntityAction(ctx.getTenantId(), entity.getId(), entity, null, + ActionType.DELETED, ctx.getUser()); }); ctx.registerDeleted(entityType); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java index feff708983..0cc73cd945 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java @@ -103,7 +103,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu private final Map> pendingRequestMap = new HashMap<>(); private final Map> chunkedMsgs = new ConcurrentHashMap<>(); - @Value("${queue.vc.request-timeout:60000}") + @Value("${queue.vc.request-timeout:180000}") private int requestTimeout; @Value("${queue.vc.msg-chunk-size:500000}") private int msgChunkSize; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index 1603d33bfd..005ebd268a 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -25,6 +25,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmComment; @@ -44,15 +45,16 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmTrigger; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; -import org.thingsboard.server.common.data.notification.rule.trigger.AlarmTrigger; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmService; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; @@ -235,6 +237,11 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService }); } + @Override + public PageData findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink) { + return alarmService.findAlarmTypesByTenantId(tenantId, pageLink); + } + private void onAlarmUpdated(AlarmApiCallResult result) { wsCallBackExecutor.submit(() -> { AlarmInfo alarm = result.getAlarm(); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 3f5e52796a..df3a15e765 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -78,6 +79,9 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer private ExecutorService tsCallBackExecutor; + @Value("${sql.ts.value_no_xss_validation:false}") + private boolean valueNoXssValidation; + public DefaultTelemetrySubscriptionService(AttributesService attrService, TimeseriesService tsService, @Lazy TbEntityViewService tbEntityViewService, @@ -135,7 +139,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer checkInternalEntity(entityId); boolean sysTenant = TenantId.SYS_TENANT_ID.equals(tenantId) || tenantId == null; if (sysTenant || apiUsageStateService.getApiUsageState(tenantId).isDbStorageEnabled()) { - KvUtils.validate(ts); + KvUtils.validate(ts, valueNoXssValidation); if (saveLatest) { saveAndNotifyInternal(tenantId, entityId, ts, ttl, getCallback(tenantId, customerId, sysTenant, callback)); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index d0d996fe1f..89a7480708 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -53,6 +53,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.ota.OtaPackageUtil; import org.thingsboard.server.common.data.page.PageData; @@ -346,7 +347,7 @@ public class DefaultTransportApiService implements TransportApiService { DeviceId deviceId = device.getId(); JsonNode entityNode = JacksonUtil.valueToTree(device); - TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, null); } else { JsonNode deviceAdditionalInfo = device.getAdditionalInfo(); diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java index 5154829ea6..2f84767eff 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java @@ -39,7 +39,9 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.action.EntityActionService; import java.util.Date; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; @TbCoreComponent @@ -86,20 +88,25 @@ public class AlarmsCleanUpService { PageLink removalBatchRequest = new PageLink(removalBatchSize, 0); long totalRemoved = 0; + Set typesToRemove = new HashSet<>(); while (true) { PageData toRemove = alarmDao.findAlarmsIdsByEndTsBeforeAndTenantId(expirationTime, tenantId, removalBatchRequest); for (AlarmId alarmId : toRemove.getData()) { relationService.deleteEntityRelations(tenantId, alarmId); - Alarm alarm = alarmService.delAlarm(tenantId, alarmId).getAlarm(); + Alarm alarm = alarmService.delAlarm(tenantId, alarmId, false).getAlarm(); if (alarm != null) { entityActionService.pushEntityActionToRuleEngine(alarm.getOriginator(), alarm, tenantId, null, ActionType.ALARM_DELETE, null); totalRemoved++; + typesToRemove.add(alarm.getType()); } } if (!toRemove.hasNext()) { break; } } + + alarmService.delAlarmTypes(tenantId, typesToRemove); + if (totalRemoved > 0) { log.info("Removed {} outdated alarm(s) for tenant {} older than {}", totalRemoved, tenantId, new Date(expirationTime)); } diff --git a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java index 4a07444bbb..0deb2f6bb7 100644 --- a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java @@ -16,8 +16,6 @@ package org.thingsboard.server.utils; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.model.DDFFileParser; -import org.eclipse.leshan.core.model.DefaultDDFFileValidator; import org.eclipse.leshan.core.model.InvalidDDFFileException; import org.eclipse.leshan.core.model.ObjectModel; import org.thingsboard.server.common.data.ResourceType; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index faeaade41b..6e745f063f 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -96,6 +96,10 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + # The recalculate_delay property recommended in a microservices architecture setup for rule-engine services. + # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. + # The delay is recommended because the initialization of rule chain actors is time-consuming. Avoiding unnecessary recalculations during a restart can enhance system performance and stability. + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" cluster: stats: @@ -249,7 +253,8 @@ cassandra: concurrent_limit: "${CASSANDRA_QUERY_CONCURRENT_LIMIT:1000}" permit_max_wait_time: "${PERMIT_MAX_WAIT_TIME:120000}" dispatcher_threads: "${CASSANDRA_QUERY_DISPATCHER_THREADS:2}" - callback_threads: "${CASSANDRA_QUERY_CALLBACK_THREADS:4}" + callback_threads: "${CASSANDRA_QUERY_CALLBACK_THREADS:4}" # Buffered rate executor (read, write), for managing I/O rate. See "nosql-*-callback" threads in JMX + result_processing_threads: "${CASSANDRA_QUERY_RESULT_PROCESSING_THREADS:50}" # Result set transformer and processing. See "cassandra-callback" threads in JMX poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}" rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}" # set all data types values except target to null for the same ts on save @@ -267,11 +272,13 @@ sql: batch_max_delay: "${SQL_ATTRIBUTES_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_ATTRIBUTES_BATCH_STATS_PRINT_MS:10000}" batch_threads: "${SQL_ATTRIBUTES_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution + value_no_xss_validation: "${SQL_ATTRIBUTES_VALUE_NO_XSS_VALIDATION:false}" ts: batch_size: "${SQL_TS_BATCH_SIZE:10000}" batch_max_delay: "${SQL_TS_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_TS_BATCH_STATS_PRINT_MS:10000}" batch_threads: "${SQL_TS_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution + value_no_xss_validation: "${SQL_TS_VALUE_NO_XSS_VALIDATION:false}" ts_latest: batch_size: "${SQL_TS_LATEST_BATCH_SIZE:10000}" batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}" @@ -490,7 +497,7 @@ cache: timeToLiveInMinutes: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_TTL:60}" maxSize: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_MAX_SIZE:100000}" versionControlTask: - timeToLiveInMinutes: "${CACHE_SPECS_VERSION_CONTROL_TASK_TTL:5}" + timeToLiveInMinutes: "${CACHE_SPECS_VERSION_CONTROL_TASK_TTL:20}" maxSize: "${CACHE_SPECS_VERSION_CONTROL_TASK_MAX_SIZE:100000}" userSettings: timeToLiveInMinutes: "${CACHE_SPECS_USER_SETTINGS_TTL:1440}" @@ -504,6 +511,9 @@ cache: resourceInfo: timeToLiveInMinutes: "${CACHE_SPECS_RESOURCE_INFO_TTL:1440}" maxSize: "${CACHE_SPECS_RESOURCE_INFO_MAX_SIZE:100000}" + alarmTypes: + timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}" + maxSize: "${CACHE_SPECS_ALARM_TYPES_MAX_SIZE:10000}" # deliberately placed outside 'specs' group above notificationRules: @@ -647,6 +657,7 @@ audit-log: "device": "${AUDIT_LOG_MASK_DEVICE:W}" "asset": "${AUDIT_LOG_MASK_ASSET:W}" "dashboard": "${AUDIT_LOG_MASK_DASHBOARD:W}" + "widget_type": "${AUDIT_LOG_MASK_WIDGET_TYPE:W}" "widgets_bundle": "${AUDIT_LOG_MASK_WIDGETS_BUNDLE:W}" "customer": "${AUDIT_LOG_MASK_CUSTOMER:W}" "user": "${AUDIT_LOG_MASK_USER:W}" @@ -989,6 +1000,35 @@ transport: enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" +# Device connectivity properties to publish telemetry +device: + connectivity: + http: + enabled: "${DEVICE_CONNECTIVITY_HTTP_ENABLED:true}" + host: "${DEVICE_CONNECTIVITY_HTTP_HOST:}" + port: "${DEVICE_CONNECTIVITY_HTTP_PORT:8080}" + https: + enabled: "${DEVICE_CONNECTIVITY_HTTPS_ENABLED:false}" + host: "${DEVICE_CONNECTIVITY_HTTPS_HOST:}" + port: "${DEVICE_CONNECTIVITY_HTTPS_PORT:443}" + mqtt: + enabled: "${DEVICE_CONNECTIVITY_MQTT_ENABLED:true}" + host: "${DEVICE_CONNECTIVITY_MQTT_HOST:}" + port: "${DEVICE_CONNECTIVITY_MQTT_PORT:1883}" + mqtts: + enabled: "${DEVICE_CONNECTIVITY_MQTTS_ENABLED:false}" + host: "${DEVICE_CONNECTIVITY_MQTTS_HOST:}" + port: "${DEVICE_CONNECTIVITY_MQTTS_PORT:8883}" + pem_cert_file: "${DEVICE_CONNECTIVITY_MQTT_SSL_PEM_CERT:mqttserver.pem}" + coap: + enabled: "${DEVICE_CONNECTIVITY_COAP_ENABLED:true}" + host: "${DEVICE_CONNECTIVITY_COAP_HOST:}" + port: "${DEVICE_CONNECTIVITY_COAP_PORT:5683}" + coaps: + enabled: "${DEVICE_CONNECTIVITY_COAPS_ENABLED:false}" + host: "${DEVICE_CONNECTIVITY_COAPS_HOST:}" + port: "${DEVICE_CONNECTIVITY_COAPS_PORT:5684}" + # Edges parameters edges: enabled: "${EDGES_ENABLED:true}" @@ -1059,6 +1099,7 @@ queue: fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + auto_offset_reset: "${TB_QUEUE_KAFKA_AUTO_OFFSET_RESET:earliest}" # earliest, latest or none use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" @@ -1178,8 +1219,8 @@ queue: topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" partitions: "${TB_QUEUE_VC_PARTITIONS:10}" poll-interval: "${TB_QUEUE_VC_INTERVAL_MS:25}" - pack-processing-timeout: "${TB_QUEUE_VC_PACK_PROCESSING_TIMEOUT_MS:60000}" - request-timeout: "${TB_QUEUE_VC_REQUEST_TIMEOUT:60000}" + pack-processing-timeout: "${TB_QUEUE_VC_PACK_PROCESSING_TIMEOUT_MS:180000}" + request-timeout: "${TB_QUEUE_VC_REQUEST_TIMEOUT:180000}" msg-chunk-size: "${TB_QUEUE_VC_MSG_CHUNK_SIZE:250000}" js: # JS Eval request topic @@ -1255,6 +1296,8 @@ queue: failure-percentage: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRY_PAUSE:5}" # Time in seconds to wait in consumer thread before retries; max-pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:5}" # Max allowed time in seconds for pause between retries. + # After a queue is deleted (or profile's isolation option was disabled), Rule Engine will continue reading related topics during this period, before deleting the actual topics + topic-deletion-delay: "${TB_QUEUE_RULE_ENGINE_TOPIC_DELETION_DELAY_SEC:30}" transport: # For high priority notifications that require minimum latency and processing time notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" @@ -1268,6 +1311,10 @@ service: type: "${TB_SERVICE_TYPE:monolith}" # monolith or tb-core or tb-rule-engine # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" + rule_engine: + # Comma-separated list of tenant profiles ids assigned to this Rule Engine. + # This Rule Engine will only be responsible for tenants with these profiles (in case 'isolation' option is enabled in profile). + assigned_tenant_profiles: "${TB_RULE_ENGINE_ASSIGNED_TENANT_PROFILES:}" metrics: # Enable/disable actuator metrics. diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index d5eabdff2a..71c244890d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -74,6 +74,17 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { Mockito.reset(tbClusterService, auditLogService); } + protected void testNotifyAssignUnassignEntityAllOneTime(HasName entity, EntityId entityId, EntityId originatorId, + TenantId tenantId, CustomerId customerId, UserId userId, String userName, + ActionType actionType, ActionType actionTypeEdge, Object... additionalInfo) { + int cntTime = 1; + testNotificationMsgToEdgeServiceTime(entityId, tenantId, actionTypeEdge, cntTime); + testLogEntityAction(entity, originatorId, tenantId, customerId, userId, userName, actionType, cntTime, additionalInfo); + ArgumentMatcher matcherOriginatorId = argument -> argument.equals(originatorId); + testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, entity, cntTime); + Mockito.reset(tbClusterService, auditLogService); + } + protected void testNotifyEntityAllOneTimeRelation(EntityRelation relation, TenantId tenantId, CustomerId customerId, UserId userId, String userName, ActionType actionType, Object... additionalInfo) { @@ -115,9 +126,9 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { protected void testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(HasName entity, EntityId entityId, EntityId originatorId, TenantId tenantId, CustomerId customerId, UserId userId, String userName, - ActionType actionType, Object... additionalInfo) { + ActionType actionType, ActionType actionTypeEdge, Object... additionalInfo) { int cntTime = 1; - testNotificationMsgToEdgeServiceTime(entityId, tenantId, actionType, cntTime); + testNotificationMsgToEdgeServiceTime(entityId, tenantId, actionTypeEdge, cntTime); testLogEntityActionEntityEqClass(entity, originatorId, tenantId, customerId, userId, userName, actionType, cntTime, additionalInfo); ArgumentMatcher matcherOriginatorId = argument -> argument.equals(originatorId); testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, entity, cntTime); @@ -163,10 +174,10 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { protected void testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(HasName entity, HasName originator, TenantId tenantId, CustomerId customerId, UserId userId, String userName, - ActionType actionType, ActionType actionTypeEdge, + ActionType actionType, int cntTime, int cntTimeEdge, int cntTimeRuleEngine, Object... additionalInfo) { EntityId originatorId = createEntityId_NULL_UUID(originator); - testSendNotificationMsgToEdgeServiceTimeEntityEqAny(tenantId, actionTypeEdge, cntTimeEdge); + testSendNotificationMsgToEdgeServiceTimeEntityEqAny(tenantId, actionType, cntTimeEdge); ArgumentMatcher matcherEntityClassEquals = argument -> argument.getClass().equals(entity.getClass()); ArgumentMatcher matcherOriginatorId = argument -> argument.getClass().equals(originatorId.getClass()); ArgumentMatcher matcherCustomerId = customerId == null ? diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java index 49e3550a1e..656936b175 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java @@ -25,13 +25,13 @@ import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.dao.rule.RuleChainService; -import java.io.IOException; import java.util.function.Predicate; /** @@ -82,8 +82,8 @@ public abstract class AbstractRuleEngineControllerTest extends AbstractControlle } } - protected Predicate filterByCustomEvent() { - return event -> event.getBody().get("msgType").textValue().equals("CUSTOM"); + protected Predicate filterByPostTelemetryEventType() { + return event -> event.getBody().get("msgType").textValue().equals(TbMsgType.POST_TELEMETRY_REQUEST.name()); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index ec32ba47ef..151e779ae0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -96,6 +96,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; @@ -189,7 +190,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected String username; protected TenantId tenantId; + protected TenantProfileId tenantProfileId; protected UserId tenantAdminUserId; + protected User tenantAdminUser; protected CustomerId tenantAdminCustomerId; protected CustomerId customerId; protected TenantId differentTenantId; @@ -270,15 +273,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class); Assert.assertNotNull(savedTenant); tenantId = savedTenant.getId(); + tenantProfileId = savedTenant.getTenantProfileId(); - User tenantAdmin = new User(); - tenantAdmin.setAuthority(Authority.TENANT_ADMIN); - tenantAdmin.setTenantId(tenantId); - tenantAdmin.setEmail(TENANT_ADMIN_EMAIL); + tenantAdminUser = new User(); + tenantAdminUser.setAuthority(Authority.TENANT_ADMIN); + tenantAdminUser.setTenantId(tenantId); + tenantAdminUser.setEmail(TENANT_ADMIN_EMAIL); - tenantAdmin = createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD); - tenantAdminUserId = tenantAdmin.getId(); - tenantAdminCustomerId = tenantAdmin.getCustomerId(); + tenantAdminUser = createUserAndLogin(tenantAdminUser, TENANT_ADMIN_PASSWORD); + tenantAdminUserId = tenantAdminUser.getId(); + tenantAdminCustomerId = tenantAdminUser.getCustomerId(); Customer customer = new Customer(); customer.setTitle("Customer"); @@ -793,6 +797,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass); } + protected T doDeleteAsync(String urlTemplate, Class responseClass, String... params) throws Exception { + return readResponse(doDeleteAsync(urlTemplate, DEFAULT_TIMEOUT, params).andExpect(status().isOk()), responseClass); + } + protected ResultActions doPost(String urlTemplate, String... params) throws Exception { MockHttpServletRequestBuilder postRequest = post(urlTemplate); setJwtToken(postRequest); @@ -825,6 +833,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(deleteRequest); } + protected ResultActions doDeleteAsync(String urlTemplate, Long timeout, String... params) throws Exception { + MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate, params); + setJwtToken(deleteRequest); +// populateParams(deleteRequest, params); + MvcResult result = mockMvc.perform(deleteRequest).andReturn(); + result.getAsyncResult(timeout); + return mockMvc.perform(asyncDispatch(result)); + } + protected void populateParams(MockHttpServletRequestBuilder request, String... params) { if (params != null && params.length > 0) { Assert.assertEquals(0, params.length % 2); @@ -1022,13 +1039,20 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return (DeviceActorMessageProcessor) ReflectionTestUtils.getField(actor, "processor"); } - protected void updateDefaultTenantProfile(Consumer updater) throws ThingsboardException { - TenantProfile tenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); - TenantProfileData profileData = tenantProfile.getProfileData(); - DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration(); - updater.accept(profileConfiguration); - tenantProfile.setProfileData(profileData); - tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, null); + protected void updateDefaultTenantProfileConfig(Consumer updater) throws ThingsboardException { + updateDefaultTenantProfile(tenantProfile -> { + TenantProfileData profileData = tenantProfile.getProfileData(); + DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration(); + updater.accept(profileConfiguration); + tenantProfile.setProfileData(profileData); + }); + } + + protected void updateDefaultTenantProfile(Consumer updater) throws ThingsboardException { + TenantProfile oldTenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); + TenantProfile tenantProfile = JacksonUtil.clone(oldTenantProfile); + updater.accept(tenantProfile); + tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, oldTenantProfile); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java index 50761be096..0e6eb16713 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -31,6 +32,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; @@ -44,8 +46,13 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; +import java.util.Objects; +import java.util.concurrent.TimeUnit; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -158,8 +165,10 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertEquals(alarm.getAckTs(), updatedAlarm.getAckTs()); foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ACK); + + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundAlarm, customerDevice, tenantId, + customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ACK, 1, 1, 1); + Mockito.reset(tbClusterService, auditLogService); alarm = updatedAlarm; alarm.setCleared(true); @@ -170,8 +179,10 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertEquals(alarm.getClearTs(), updatedAlarm.getClearTs()); foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_CLEAR); + + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundAlarm, customerDevice, tenantId, + customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_CLEAR, 1, 1, 1); + Mockito.reset(tbClusterService, auditLogService); alarm = updatedAlarm; alarm.setAssigneeId(tenantAdminUserId); @@ -182,8 +193,10 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertEquals(alarm.getAssignTs(), updatedAlarm.getAssignTs()); foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED); + + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundAlarm, customerDevice, tenantId, + customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED, 1, 1, 1); + Mockito.reset(tbClusterService, auditLogService); alarm = updatedAlarm; alarm.setAssigneeId(null); @@ -194,8 +207,11 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertEquals(alarm.getAssignTs(), updatedAlarm.getAssignTs()); foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGNED); + + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundAlarm, customerDevice, tenantId, + customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGNED, 1, 1, 1); + Mockito.reset(tbClusterService, auditLogService); + } @Test @@ -241,7 +257,7 @@ public class AlarmControllerTest extends AbstractControllerTest { doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk()); - testNotifyEntityOneTimeMsgToEdgeServiceNever(new Alarm(alarm), alarm.getId(), alarm.getOriginator(), + testNotifyEntityAllOneTime(new Alarm(alarm), alarm.getId(), alarm.getOriginator(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED); } @@ -254,7 +270,7 @@ public class AlarmControllerTest extends AbstractControllerTest { doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk()); - testNotifyEntityOneTimeMsgToEdgeServiceNever(new Alarm(alarm), alarm.getId(), alarm.getOriginator(), + testNotifyEntityAllOneTime(new Alarm(alarm), alarm.getId(), alarm.getOriginator(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED); } @@ -532,7 +548,7 @@ public class AlarmControllerTest extends AbstractControllerTest { } @Test - public void testUnassignAlarmOnUserRemoving() throws Exception { + public void testUnassignTenantUserAlarmOnUserRemoving() throws Exception { loginDifferentTenant(); User user = new User(); @@ -554,12 +570,113 @@ public class AlarmControllerTest extends AbstractControllerTest { alarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); Assert.assertNotNull(alarm); + AlarmId alarmId = alarm.getId(); + + long beforeAssignmentTs = System.currentTimeMillis(); + + doPost("/api/alarm/" + alarmId.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk()); + Alarm foundAlarm = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); + + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); + + beforeAssignmentTs = System.currentTimeMillis(); + + loginSysAdmin(); + + doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk()); + + loginDifferentTenant(); + + foundAlarm = Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class), Objects::nonNull); + + Assert.assertNotNull(foundAlarm); + Assert.assertNull(foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); + } + + @Test + public void testUnassignAlarmOnUserRemoving() throws Exception { + loginDifferentTenant(); + + User user = new User(); + user.setAuthority(Authority.TENANT_ADMIN); + user.setTenantId(tenantId); + user.setEmail("tenantForAssign@thingsboard.org"); + User savedUser = createUser(user, "password"); + + Device device = createDevice("Different tenant device", "default", "differentTenantTest"); + + Alarm alarm = Alarm.builder() + .type(TEST_ALARM_TYPE) + .tenantId(savedDifferentTenant.getId()) + .originator(device.getId()) + .severity(AlarmSeverity.MAJOR) + .build(); + alarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(alarm); + AlarmId alarmId = alarm.getId(); + alarm = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); + Assert.assertNotNull(alarm); + long beforeAssignmentTs = System.currentTimeMillis(); + + doPost("/api/alarm/" + alarmId.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk()); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); + + beforeAssignmentTs = System.currentTimeMillis(); + + doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk()); + + foundAlarm = Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class), Objects::nonNull); + + Assert.assertNotNull(foundAlarm); + Assert.assertNull(foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); + } + + @Test + public void testUnassignAlarmOnCustomerRemoving() throws Exception { + createDifferentTenantCustomer(); + loginDifferentTenant(); + + User user = new User(); + user.setAuthority(Authority.CUSTOMER_USER); + user.setTenantId(tenantId); + user.setCustomerId(differentTenantCustomerId); + user.setEmail("customerForAssign@thingsboard.org"); + User savedUser = createUser(user, "password"); + + Device device = createDevice("Different customer device", "default", "differentTenantTest"); + + Device assignedDevice = doPost("/api/customer/" + differentTenantCustomerId.getId() + + "/device/" + device.getId().getId(), Device.class); + Assert.assertEquals(differentTenantCustomerId, assignedDevice.getCustomerId()); + + Alarm alarm = Alarm.builder() + .type(TEST_ALARM_TYPE) + .tenantId(savedDifferentTenant.getId()) + .customerId(differentTenantCustomerId) + .originator(device.getId()) + .severity(AlarmSeverity.MAJOR) + .build(); + alarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(alarm); + AlarmId alarmId = alarm.getId(); + + alarm = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); + Assert.assertNotNull(alarm); Mockito.reset(tbClusterService, auditLogService); long beforeAssignmentTs = System.currentTimeMillis(); - doPost("/api/alarm/" + alarm.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk()); - AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + doPost("/api/alarm/" + alarmId.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk()); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); Assert.assertNotNull(foundAlarm); Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); @@ -568,9 +685,11 @@ public class AlarmControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); - doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk()); + doDelete("/api/customer/" + differentTenantCustomerId.getId()).andExpect(status().isOk()); + + foundAlarm = Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class), Objects::nonNull); - foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); Assert.assertNotNull(foundAlarm); Assert.assertNull(foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); @@ -738,4 +857,110 @@ public class AlarmControllerTest extends AbstractControllerTest { doPost("/api/alarm", alarm).andExpect(status().isForbidden()); } + @Test + public void testSaveAlarmTypes() throws Exception { + loginTenantAdmin(); + + List types = new ArrayList<>(); + + for (int i = 1; i < 13; i++) { + types.add(createAlarm(TEST_ALARM_TYPE + i).getType()); + } + + Device device = new Device(); + device.setName("Test device 2"); + device.setCustomerId(customerId); + customerDevice = doPost("/api/device", device, Device.class); + + for (int i = 1; i < 10; i++) { + createAlarm(TEST_ALARM_TYPE + i); + } + + List foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() { + }) + .getData() + .stream() + .map(EntitySubtype::getType) + .collect(Collectors.toList()); + + Collections.sort(types); + Collections.sort(foundTypes); + + Assert.assertEquals(types, foundTypes); + } + + @Test + public void testDeleteAlarmTypes() throws Exception { + loginTenantAdmin(); + + List alarms = new ArrayList<>(); + + for (int i = 1; i < 13; i++) { + alarms.add(createAlarm(TEST_ALARM_TYPE + i)); + } + + Device device = new Device(); + device.setName("Test device 2"); + device.setCustomerId(customerId); + customerDevice = doPost("/api/device", device, Device.class); + + for (int i = 1; i < 14; i++) { + alarms.add(createAlarm(TEST_ALARM_TYPE + i)); + } + + List expectedTypes = alarms.stream().map(AlarmInfo::getType).distinct().sorted().collect(Collectors.toList()); + + List foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() { + }) + .getData() + .stream() + .map(EntitySubtype::getType) + .sorted() + .collect(Collectors.toList()); + + Assert.assertEquals(13, foundTypes.size()); + Assert.assertEquals(expectedTypes, foundTypes); + + for (int i = 0; i < 12; i++) { + doDelete("/api/alarm/" + alarms.get(i).getId()).andExpect(status().isOk()); + } + + foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() { + }) + .getData() + .stream() + .map(EntitySubtype::getType) + .sorted() + .collect(Collectors.toList()); + + Assert.assertEquals(13, foundTypes.size()); + Assert.assertEquals(expectedTypes, foundTypes); + + doDelete("/api/alarm/" + alarms.get(12).getId()).andExpect(status().isOk()); + + foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() { + }) + .getData() + .stream() + .map(EntitySubtype::getType) + .sorted() + .collect(Collectors.toList()); + + Assert.assertEquals(12, foundTypes.size()); + + for (int i = 13; i < alarms.size(); i++) { + doDelete("/api/alarm/" + alarms.get(i).getId()).andExpect(status().isOk()); + } + + foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() { + }) + .getData() + .stream() + .map(EntitySubtype::getType) + .sorted() + .collect(Collectors.toList()); + + Assert.assertTrue(foundTypes.isEmpty()); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java index 40b1707271..88a8b939b8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java @@ -50,7 +50,6 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.stats.DefaultRuleEngineStatisticsService; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import static org.hamcrest.Matchers.containsString; @@ -114,8 +113,8 @@ public class AssetControllerTest extends AbstractControllerTest { Asset savedAsset = doPost("/api/asset", asset, Asset.class); - testNotifyEntityOneTimeMsgToEdgeServiceNever(savedAsset, savedAsset.getId(), savedAsset.getId(), - savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED); + testNotifyEntityAllOneTime(savedAsset, savedAsset.getId(), savedAsset.getId(), savedTenant.getId(), + tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED); Assert.assertNotNull(savedAsset); Assert.assertNotNull(savedAsset.getId()); @@ -130,8 +129,8 @@ public class AssetControllerTest extends AbstractControllerTest { savedAsset.setName("My new asset"); doPost("/api/asset", savedAsset, Asset.class); - testNotifyEntityAllOneTime(savedAsset, savedAsset.getId(), savedAsset.getId(), - savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UPDATED); + testNotifyEntityAllOneTime(savedAsset, savedAsset.getId(), savedAsset.getId(), savedTenant.getId(), + tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UPDATED); Asset foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class); Assert.assertEquals(foundAsset.getName(), savedAsset.getName()); @@ -229,7 +228,8 @@ public class AssetControllerTest extends AbstractControllerTest { @Test public void testFindAssetTypesByTenantId() throws Exception { - List assets = new ArrayList<>(); + AssetProfile assetProfile = createAssetProfile("typeB"); + assetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class); Mockito.reset(tbClusterService, auditLogService); @@ -238,24 +238,25 @@ public class AssetControllerTest extends AbstractControllerTest { Asset asset = new Asset(); asset.setName("My asset B" + i); asset.setType("typeB"); - assets.add(doPost("/api/asset", asset, Asset.class)); + asset.setAssetProfileId(assetProfile.getId()); + doPost("/api/asset", asset, Asset.class); } - testNotifyManyEntityManyTimeMsgToEdgeServiceNever(new Asset(), new Asset(), + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new Asset(), new Asset(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED, cntTime); + ActionType.ADDED, cntTime, cntTime, cntTime); for (int i = 0; i < 7; i++) { Asset asset = new Asset(); asset.setName("My asset C" + i); asset.setType("typeC"); - assets.add(doPost("/api/asset", asset, Asset.class)); + doPost("/api/asset", asset, Asset.class); } for (int i = 0; i < 9; i++) { Asset asset = new Asset(); asset.setName("My asset A" + i); asset.setType("typeA"); - assets.add(doPost("/api/asset", asset, Asset.class)); + doPost("/api/asset", asset, Asset.class); } List assetTypes = doGetTyped("/api/asset/types", new TypeReference>() { @@ -280,7 +281,7 @@ public class AssetControllerTest extends AbstractControllerTest { doDelete("/api/asset/" + savedAsset.getId().getId().toString()) .andExpect(status().isOk()); - testNotifyEntityOneTimeMsgToEdgeServiceNever(savedAsset, savedAsset.getId(), savedAsset.getId(), + testNotifyEntityAllOneTime(savedAsset, savedAsset.getId(), savedAsset.getId(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.DELETED, savedAsset.getId().getId().toString()); @@ -342,7 +343,7 @@ public class AssetControllerTest extends AbstractControllerTest { Asset savedAsset = doPost("/api/asset", asset, Asset.class); Assert.assertEquals("default", savedAsset.getType()); - testNotifyEntityOneTimeMsgToEdgeServiceNever(savedAsset, savedAsset.getId(), savedAsset.getId(), + testNotifyEntityAllOneTime(savedAsset, savedAsset.getId(), savedAsset.getId(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED); } @@ -380,9 +381,9 @@ public class AssetControllerTest extends AbstractControllerTest { + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); Assert.assertEquals(savedCustomer.getId(), assignedAsset.getCustomerId()); - testNotifyEntityAllOneTime(assignedAsset, assignedAsset.getId(), assignedAsset.getId(), + testNotifyAssignUnassignEntityAllOneTime(assignedAsset, assignedAsset.getId(), assignedAsset.getId(), savedTenant.getId(), savedCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ASSIGNED_TO_CUSTOMER, assignedAsset.getId().toString(), savedCustomer.getId().toString(), savedCustomer.getTitle()); + ActionType.ASSIGNED_TO_CUSTOMER, ActionType.UPDATED, assignedAsset.getId().toString(), savedCustomer.getId().toString(), savedCustomer.getTitle()); Asset foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class); Assert.assertEquals(savedCustomer.getId(), foundAsset.getCustomerId()); @@ -393,9 +394,9 @@ public class AssetControllerTest extends AbstractControllerTest { doDelete("/api/customer/asset/" + savedAsset.getId().getId().toString(), Asset.class); Assert.assertEquals(ModelConstants.NULL_UUID, unassignedAsset.getCustomerId().getId()); - testNotifyEntityAllOneTime(savedAsset, savedAsset.getId(), savedAsset.getId(), + testNotifyAssignUnassignEntityAllOneTime(savedAsset, savedAsset.getId(), savedAsset.getId(), savedTenant.getId(), savedCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.UNASSIGNED_FROM_CUSTOMER, savedAsset.getId().toString(), savedCustomer.getId().toString(), savedCustomer.getTitle()); + ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UPDATED, savedAsset.getId().toString(), savedCustomer.getId().toString(), savedCustomer.getTitle()); foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class); Assert.assertEquals(ModelConstants.NULL_UUID, foundAsset.getCustomerId().getId()); @@ -415,9 +416,10 @@ public class AssetControllerTest extends AbstractControllerTest { Customer publicCustomer = doGet("/api/customer/" + assignedAsset.getCustomerId(), Customer.class); Assert.assertTrue(publicCustomer.isPublic()); - testNotifyEntityAllOneTime(assignedAsset, assignedAsset.getId(), assignedAsset.getId(), + testNotifyAssignUnassignEntityAllOneTime(assignedAsset, assignedAsset.getId(), assignedAsset.getId(), savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ASSIGNED_TO_CUSTOMER, assignedAsset.getId().toString(), publicCustomer.getId().toString(), publicCustomer.getTitle()); + ActionType.ASSIGNED_TO_CUSTOMER, ActionType.UPDATED, assignedAsset.getId().toString(), + publicCustomer.getId().toString(), publicCustomer.getTitle()); Asset foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class); Assert.assertEquals(publicCustomer.getId(), foundAsset.getCustomerId()); @@ -428,9 +430,10 @@ public class AssetControllerTest extends AbstractControllerTest { doDelete("/api/customer/asset/" + savedAsset.getId().getId().toString(), Asset.class); Assert.assertEquals(ModelConstants.NULL_UUID, unassignedAsset.getCustomerId().getId()); - testNotifyEntityAllOneTime(savedAsset, savedAsset.getId(), savedAsset.getId(), + testNotifyAssignUnassignEntityAllOneTime(savedAsset, savedAsset.getId(), savedAsset.getId(), savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.UNASSIGNED_FROM_CUSTOMER, savedAsset.getId().toString(), publicCustomer.getId().toString(), publicCustomer.getTitle()); + ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UPDATED, savedAsset.getId().toString(), + publicCustomer.getId().toString(), publicCustomer.getTitle()); foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class); Assert.assertEquals(ModelConstants.NULL_UUID, foundAsset.getCustomerId().getId()); @@ -513,7 +516,7 @@ public class AssetControllerTest extends AbstractControllerTest { } List loadedAssets = new ArrayList<>(); PageLink pageLink = new PageLink(23); - PageData pageData = null; + PageData pageData; do { pageData = doGetTypedWithPageLink("/api/tenant/assets?", new TypeReference>() { @@ -524,14 +527,14 @@ public class AssetControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - testNotifyManyEntityManyTimeMsgToEdgeServiceNever(new Asset(), new Asset(), + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new Asset(), new Asset(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED, cntEntity); + ActionType.ADDED, cntEntity, cntEntity, cntEntity); loadedAssets.removeIf(asset -> asset.getType().equals(DefaultRuleEngineStatisticsService.TB_SERVICE_QUEUE)); - Collections.sort(assets, idComparator); - Collections.sort(loadedAssets, idComparator); + assets.sort(idComparator); + loadedAssets.sort(idComparator); Assert.assertEquals(assets, loadedAssets); } @@ -574,8 +577,8 @@ public class AssetControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(assetsTitle1, idComparator); - Collections.sort(loadedAssetsTitle1, idComparator); + assetsTitle1.sort(idComparator); + loadedAssetsTitle1.sort(idComparator); Assert.assertEquals(assetsTitle1, loadedAssetsTitle1); @@ -591,8 +594,8 @@ public class AssetControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(assetsTitle2, idComparator); - Collections.sort(loadedAssetsTitle2, idComparator); + assetsTitle2.sort(idComparator); + loadedAssetsTitle2.sort(idComparator); Assert.assertEquals(assetsTitle2, loadedAssetsTitle2); @@ -661,8 +664,8 @@ public class AssetControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(assetsType1, idComparator); - Collections.sort(loadedAssetsType1, idComparator); + assetsType1.sort(idComparator); + loadedAssetsType1.sort(idComparator); Assert.assertEquals(assetsType1, loadedAssetsType1); @@ -678,8 +681,8 @@ public class AssetControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(assetsType2, idComparator); - Collections.sort(loadedAssetsType2, idComparator); + assetsType2.sort(idComparator); + loadedAssetsType2.sort(idComparator); Assert.assertEquals(assetsType2, loadedAssetsType2); @@ -738,8 +741,8 @@ public class AssetControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(assets, idComparator); - Collections.sort(loadedAssets, idComparator); + assets.sort(idComparator); + loadedAssets.sort(idComparator); Assert.assertEquals(assets, loadedAssets); } @@ -791,8 +794,8 @@ public class AssetControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(assetsTitle1, idComparator); - Collections.sort(loadedAssetsTitle1, idComparator); + assetsTitle1.sort(idComparator); + loadedAssetsTitle1.sort(idComparator); Assert.assertEquals(assetsTitle1, loadedAssetsTitle1); @@ -808,8 +811,8 @@ public class AssetControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(assetsTitle2, idComparator); - Collections.sort(loadedAssetsTitle2, idComparator); + assetsTitle2.sort(idComparator); + loadedAssetsTitle2.sort(idComparator); Assert.assertEquals(assetsTitle2, loadedAssetsTitle2); @@ -887,8 +890,8 @@ public class AssetControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(assetsType1, idComparator); - Collections.sort(loadedAssetsType1, idComparator); + assetsType1.sort(idComparator); + loadedAssetsType1.sort(idComparator); Assert.assertEquals(assetsType1, loadedAssetsType1); @@ -904,8 +907,8 @@ public class AssetControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(assetsType2, idComparator); - Collections.sort(loadedAssetsType2, idComparator); + assetsType2.sort(idComparator); + loadedAssetsType2.sort(idComparator); Assert.assertEquals(assetsType2, loadedAssetsType2); diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java index c84b058cc7..38420c3e32 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java @@ -355,7 +355,7 @@ public class AssetProfileControllerTest extends AbstractControllerTest { testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new AssetProfile(), new AssetProfile(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED, ActionType.ADDED, cntEntity, cntEntity, cntEntity); + ActionType.ADDED, cntEntity, cntEntity, cntEntity); Mockito.reset(tbClusterService, auditLogService); List loadedAssetProfiles = new ArrayList<>(); @@ -384,7 +384,7 @@ public class AssetProfileControllerTest extends AbstractControllerTest { testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(loadedAssetProfiles.get(0), loadedAssetProfiles.get(0), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.DELETED, ActionType.DELETED, cntEntity, cntEntity, cntEntity, loadedAssetProfiles.get(0).getId().getId().toString()); + ActionType.DELETED, cntEntity, cntEntity, cntEntity, loadedAssetProfiles.get(0).getId().getId().toString()); pageLink = new PageLink(17); pageData = doGetTypedWithPageLink("/api/assetProfiles?", diff --git a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java index 125e533471..f5c33debb2 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java @@ -117,8 +117,8 @@ public class CustomerControllerTest extends AbstractControllerTest { Customer savedCustomer = doPost("/api/customer", customer, Customer.class); - testNotifyEntityOneTimeMsgToEdgeServiceNever(savedCustomer, savedCustomer.getId(), savedCustomer.getId(), - savedCustomer.getTenantId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + testNotifyEntityAllOneTime(savedCustomer, savedCustomer.getId(), savedCustomer.getId(), savedCustomer.getTenantId(), + new CustomerId(CustomerId.NULL_UUID), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED); Assert.assertNotNull(savedCustomer); @@ -242,7 +242,7 @@ public class CustomerControllerTest extends AbstractControllerTest { doDelete("/api/customer/" + savedCustomer.getId().getId().toString()) .andExpect(status().isOk()); - testNotifyEntityBroadcastEntityStateChangeEventOneTimeMsgToEdgeServiceNever(savedCustomer, savedCustomer.getId(), + testNotifyEntityBroadcastEntityStateChangeEventOneTime(savedCustomer, savedCustomer.getId(), savedCustomer.getId(), savedCustomer.getTenantId(), savedCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.DELETED, savedCustomer.getId().getId().toString()); } @@ -272,7 +272,7 @@ public class CustomerControllerTest extends AbstractControllerTest { doDelete("/api/customer/" + savedCustomer.getId().getId().toString()) .andExpect(status().isOk()); - testNotifyEntityBroadcastEntityStateChangeEventOneTimeMsgToEdgeServiceNever(savedCustomer, savedCustomer.getId(), + testNotifyEntityBroadcastEntityStateChangeEventOneTime(savedCustomer, savedCustomer.getId(), savedCustomer.getId(), savedCustomer.getTenantId(), savedCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.DELETED, savedCustomer.getId().getId().toString()); @@ -332,9 +332,9 @@ public class CustomerControllerTest extends AbstractControllerTest { } List customers = Futures.allAsList(futures).get(TIMEOUT, TimeUnit.SECONDS); - testNotifyManyEntityManyTimeMsgToEdgeServiceNever(new Customer(), new Customer(), + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new Customer(), new Customer(), tenantId, tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED, cntEntity); + ActionType.ADDED, cntEntity, cntEntity, cntEntity); List loadedCustomers = new ArrayList<>(135); PageLink pageLink = new PageLink(23); diff --git a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java index 9f8e63e3b0..6f0d6f9465 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java @@ -46,7 +46,6 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import static org.hamcrest.Matchers.containsString; @@ -154,7 +153,7 @@ public class DashboardControllerTest extends AbstractControllerTest { doDelete("/api/dashboard/" + savedDashboard.getId().getId().toString()).andExpect(status().isOk()); - testNotifyEntityOneTimeMsgToEdgeServiceNever(savedDashboard, savedDashboard.getId(), savedDashboard.getId(), + testNotifyEntityAllOneTime(savedDashboard, savedDashboard.getId(), savedDashboard.getId(), savedDashboard.getTenantId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.DELETED, savedDashboard.getId().getId().toString()); @@ -198,7 +197,7 @@ public class DashboardControllerTest extends AbstractControllerTest { testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(assignedDashboard, assignedDashboard.getId(), assignedDashboard.getId(), savedTenant.getId(), savedCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ASSIGNED_TO_CUSTOMER, - assignedDashboard .getId().getId().toString(), savedCustomer.getId().getId().toString(), savedCustomer.getTitle()); + ActionType.UPDATED, assignedDashboard.getId().getId().toString(), savedCustomer.getId().getId().toString(), savedCustomer.getTitle()); Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); Assert.assertTrue(foundDashboard.getAssignedCustomers().contains(savedCustomer.toShortCustomerInfo())); @@ -210,7 +209,7 @@ public class DashboardControllerTest extends AbstractControllerTest { testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(assignedDashboard, assignedDashboard.getId(), assignedDashboard.getId(), savedTenant.getId(), savedCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UNASSIGNED_FROM_CUSTOMER, - unassignedDashboard.getId().getId().toString(), savedCustomer.getId().getId().toString(), savedCustomer.getTitle()); + ActionType.UPDATED, unassignedDashboard.getId().getId().toString(), savedCustomer.getId().getId().toString(), savedCustomer.getTitle()); Assert.assertTrue(unassignedDashboard.getAssignedCustomers() == null || unassignedDashboard.getAssignedCustomers().isEmpty()); @@ -241,7 +240,7 @@ public class DashboardControllerTest extends AbstractControllerTest { testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(assignedDashboard, assignedDashboard.getId(), assignedDashboard.getId(), savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ASSIGNED_TO_CUSTOMER, - assignedDashboard .getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle()); + ActionType.UPDATED, assignedDashboard .getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle()); Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); Assert.assertTrue(foundDashboard.getAssignedCustomers().contains(publicCustomer.toShortCustomerInfo())); @@ -253,7 +252,7 @@ public class DashboardControllerTest extends AbstractControllerTest { testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(assignedDashboard, assignedDashboard.getId(), assignedDashboard.getId(), savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UNASSIGNED_FROM_CUSTOMER, - unassignedDashboard.getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle()); + ActionType.UPDATED, unassignedDashboard.getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle()); Assert.assertTrue(unassignedDashboard.getAssignedCustomers() == null || unassignedDashboard.getAssignedCustomers().isEmpty()); @@ -339,9 +338,9 @@ public class DashboardControllerTest extends AbstractControllerTest { dashboards.add(new DashboardInfo(doPost("/api/dashboard", dashboard, Dashboard.class))); } - testNotifyManyEntityManyTimeMsgToEdgeServiceNever(new Dashboard(), new Dashboard(), + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new Dashboard(), new Dashboard(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED, cntEntity); + ActionType.ADDED, cntEntity, cntEntity, cntEntity); List loadedDashboards = new ArrayList<>(); PageLink pageLink = new PageLink(24); @@ -356,8 +355,8 @@ public class DashboardControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(dashboards, idComparator); - Collections.sort(loadedDashboards, idComparator); + dashboards.sort(idComparator); + loadedDashboards.sort(idComparator); Assert.assertEquals(dashboards, loadedDashboards); } @@ -400,8 +399,8 @@ public class DashboardControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(dashboardsTitle1, idComparator); - Collections.sort(loadedDashboardsTitle1, idComparator); + dashboardsTitle1.sort(idComparator); + loadedDashboardsTitle1.sort(idComparator); Assert.assertEquals(dashboardsTitle1, loadedDashboardsTitle1); @@ -417,8 +416,8 @@ public class DashboardControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(dashboardsTitle2, idComparator); - Collections.sort(loadedDashboardsTitle2, idComparator); + dashboardsTitle2.sort(idComparator); + loadedDashboardsTitle2.sort(idComparator); Assert.assertEquals(dashboardsTitle2, loadedDashboardsTitle2); @@ -429,9 +428,9 @@ public class DashboardControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - testNotifyManyEntityManyTimeMsgToEdgeServiceNeverAdditionalInfoAny(new Dashboard(), new Dashboard(), + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAnyAdditionalInfoAny(new Dashboard(), new Dashboard(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.DELETED, cntEntity, 1); + ActionType.DELETED, ActionType.DELETED, cntEntity, cntEntity, 1); pageLink = new PageLink(4, 0, title1); pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", @@ -474,7 +473,7 @@ public class DashboardControllerTest extends AbstractControllerTest { testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new Dashboard(), new Dashboard(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED, ActionType.ASSIGNED_TO_CUSTOMER, cntEntity, cntEntity, cntEntity*2); + ActionType.ADDED, cntEntity, cntEntity, cntEntity*2); List loadedDashboards = new ArrayList<>(); PageLink pageLink = new PageLink(21); @@ -489,8 +488,8 @@ public class DashboardControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(dashboards, idComparator); - Collections.sort(loadedDashboards, idComparator); + dashboards.sort(idComparator); + loadedDashboards.sort(idComparator); Assert.assertEquals(dashboards, loadedDashboards); } diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java new file mode 100644 index 0000000000..525e18240d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java @@ -0,0 +1,405 @@ +/** + * Copyright © 2016-2023 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.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.AdditionalAnswers; +import org.mockito.Mockito; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; +import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.dao.device.DeviceDao; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FILE_NAME; + +@TestPropertySource(properties = { + "device.connectivity.https.enabled=true", + "device.connectivity.mqtts.enabled=true", + "device.connectivity.mqtts.pem_cert_file=/tmp/" + PEM_CERT_FILE_NAME, + "device.connectivity.coaps.enabled=true", +}) +@ContextConfiguration(classes = {DeviceConnectivityControllerTest.Config.class}) +@DaoSqlTest +public class DeviceConnectivityControllerTest extends AbstractControllerTest { + + private static final String DEVICE_TELEMETRY_TOPIC = "v1/devices/customTopic"; + private static final String CHECK_DOCUMENTATION = "Check documentation"; + + private static final String CERT = "-----BEGIN CERTIFICATE-----\n" + + "MIIBfzCCASmgAwIBAgIUC1dtaskm/SJLmFE2Ae+YojArg+swDQYJKoZIhvcNAQEL\n" + + "BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDgyODEzMTAzM1oXDTI0MDgy\n" + + "NzEzMTAzM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MFwwDQYJKoZIhvcNAQEBBQAD\n" + + "SwAwSAJBANpcs46MavFdv7onsxH178YgK5XbpMqzx8AKaLMP2X6UEXN0nlt5mpX5\n" + + "uCJmSwVaFn6lwTm8ThXFYOBydOQImIsCAwEAAaNTMFEwHQYDVR0OBBYEFDvN49bI\n" + + "LaWMmUZ+cMboWAaozfXTMB8GA1UdIwQYMBaAFDvN49bILaWMmUZ+cMboWAaozfXT\n" + + "MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADQQAhIQL8zPvIhQvHJocU\n" + + "tnSmDAE0iR2rJVkousA+LiORE9BnuBtBUEv5SvFUv3VYUWA0eYFoyatpDHByIm6e\n" + + "/+1c\n" + + "-----END CERTIFICATE-----\n"; + private static final String P_KEY = "-----BEGIN PRIVATE KEY-----\n" + + "MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEA2lyzjoxq8V2/uiez\n" + + "EfXvxiArldukyrPHwAposw/ZfpQRc3SeW3malfm4ImZLBVoWfqXBObxOFcVg4HJ0\n" + + "5AiYiwIDAQABAkEA1DYhPljSmc2dRcHNMphLtMWQ9iumpGRBrS2wgMzXdz2NF2+0\n" + + "4cicaaL06/Cw6XXx43s8cn7e1xZAkGtNRQuqMQIhAPbrqrcYsropURpI5HSemeha\n" + + "MJA3i67ZFaom39VSrNKJAiEA4mQ0qFKxFSh2xAOqDWDRkiCgdOS00J6hgrYJRPcI\n" + + "nXMCIQDBHGjkT72gGKYkT3PUvSGTdc3bTIXDFmZ6L3MJTGJ7OQIhAKO+6r9coCy3\n" + + "ib+ZDuSCRNK2upgR3B6Qvi020VmKfDa1AiBhCgpBlClv5OjnmC42EGxxFOaZtNQQ\n" + + "C3swkUdrR3pezg==\n" + + "-----END PRIVATE KEY-----\n"; + + ListeningExecutorService executor; + + private Tenant savedTenant; + private User tenantAdmin; + private DeviceProfileId mqttDeviceProfileId; + private DeviceProfileId coapDeviceProfileId; + + static class Config { + @Bean + @Primary + public DeviceDao deviceDao(DeviceDao deviceDao) { + return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao)); + } + } + + @Before + public void beforeTest() throws Exception { + executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); + + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + + DeviceProfile mqttProfile = new DeviceProfile(); + mqttProfile.setName("Mqtt device profile"); + mqttProfile.setType(DeviceProfileType.DEFAULT); + mqttProfile.setTransportType(DeviceTransportType.MQTT); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration()); + MqttDeviceProfileTransportConfiguration transportConfiguration = new MqttDeviceProfileTransportConfiguration(); + transportConfiguration.setDeviceTelemetryTopic(DEVICE_TELEMETRY_TOPIC); + deviceProfileData.setTransportConfiguration(transportConfiguration); + mqttProfile.setProfileData(deviceProfileData); + mqttProfile.setDefault(false); + mqttProfile.setDefaultRuleChainId(null); + + mqttDeviceProfileId = doPost("/api/deviceProfile", mqttProfile, DeviceProfile.class).getId(); + + DeviceProfile coapProfile = new DeviceProfile(); + coapProfile.setName("Coap device profile"); + coapProfile.setType(DeviceProfileType.DEFAULT); + coapProfile.setTransportType(DeviceTransportType.COAP); + DeviceProfileData deviceProfileData2 = new DeviceProfileData(); + deviceProfileData2.setConfiguration(new DefaultDeviceProfileConfiguration()); + deviceProfileData2.setTransportConfiguration(new CoapDeviceProfileTransportConfiguration()); + coapProfile.setProfileData(deviceProfileData); + coapProfile.setDefault(false); + coapProfile.setDefaultRuleChainId(null); + + coapDeviceProfileId = doPost("/api/deviceProfile", coapProfile, DeviceProfile.class).getId(); + } + + @After + public void afterTest() throws Exception { + executor.shutdownNow(); + + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testFetchPublishTelemetryCommandsForDefaultDevice() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setType("default"); + Device savedDevice = doPost("/api/device", device, Device.class); + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + assertThat(commands).hasSize(3); + JsonNode httpCommands = commands.get(HTTP); + assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost:8080/api/v1/%s/telemetry " + + "--header Content-Type:application/json --data \"{temperature:25}\"", + credentials.getCredentialsId())); + assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost:443/api/v1/%s/telemetry " + + "--header Content-Type:application/json --data \"{temperature:25}\"", + credentials.getCredentialsId())); + + + JsonNode mqttCommands = commands.get(MQTT); + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry " + + "-u %s -m \"{temperature:25}\"", + credentials.getCredentialsId())); + assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 " + + "-t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"", credentials.getCredentialsId())); + + JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + + " -p 1883 -t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"", + credentials.getCredentialsId())); + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + + "/bin/sh -c \"curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + + "mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 -t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"\"", + credentials.getCredentialsId())); + + JsonNode linuxCoapCommands = commands.get(COAP); + assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -m POST coap://localhost:5683/api/v1/%s/telemetry " + + "-t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -m POST coaps://localhost:5684/api/v1/%s/telemetry" + + " -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + } + + @Test + public void testFetchPublishTelemetryCommandsForMqttDeviceWithAccessToken() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setDeviceProfileId(mqttDeviceProfileId); + + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + assertThat(commands).hasSize(1); + + JsonNode mqttCommands = commands.get(MQTT); + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " + + "-u %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); + assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 " + + "-t %s -u %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); + + JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + + " -p 1883 -t %s -u %s -m \"{temperature:25}\"", + DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + + "/bin/sh -c \"curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + + "mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 -t %s -u %s -m \"{temperature:25}\"\"", + DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); + } + + @Test + public void testFetchPublishTelemetryCommandsForDeviceWithMqttBasicCreds() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setDeviceProfileId(mqttDeviceProfileId); + + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + credentials.setCredentialsId(null); + credentials.setCredentialsType(DeviceCredentialsType.MQTT_BASIC); + BasicMqttCredentials basicMqttCredentials = new BasicMqttCredentials(); + String clientId = "testClientId"; + String userName = "testUsername"; + String password = "testPassword"; + basicMqttCredentials.setClientId(clientId); + basicMqttCredentials.setUserName(userName); + basicMqttCredentials.setPassword(password); + credentials.setCredentialsValue(JacksonUtil.toString(basicMqttCredentials)); + doPost("/api/device/credentials", credentials) + .andExpect(status().isOk()); + + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + assertThat(commands).hasSize(1); + + JsonNode mqttCommands = commands.get(MQTT); + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " + + "-i %s -u %s -P %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); + assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 " + + "-t %s -i %s -u %s -P %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); + + JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + + " -p 1883 -t %s -i %s -u %s -P %s -m \"{temperature:25}\"", + DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + + "/bin/sh -c \"curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + + "mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 -t %s -i %s -u %s -P %s -m \"{temperature:25}\"\"", + DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); + } + + @Test + public void testFetchPublishTelemetryCommandsForDeviceWithX509Creds() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setDeviceProfileId(mqttDeviceProfileId); + + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + credentials.setCredentialsId(null); + credentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE); + credentials.setCredentialsValue("testValue"); + doPost("/api/device/credentials", credentials) + .andExpect(status().isOk()); + + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + assertThat(commands).hasSize(1); + assertThat(commands.get(MQTT).get(MQTTS).asText()).isEqualTo(CHECK_DOCUMENTATION); + assertThat(commands.get(MQTT).get(DOCKER)).isNull(); + } + + @Test + public void testFetchPublishTelemetryCommandsForCoapDevice() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setDeviceProfileId(coapDeviceProfileId); + + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + assertThat(commands).hasSize(1); + + JsonNode linuxCommands = commands.get(COAP); + assertThat(linuxCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -m POST coap://localhost:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", + credentials.getCredentialsId())); + assertThat(linuxCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -m POST coaps://localhost:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", + credentials.getCredentialsId())); + } + + @Test + public void testFetchPublishTelemetryCommandsForCoapDeviceWithX509Creds() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setDeviceProfileId(coapDeviceProfileId); + + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + credentials.setCredentialsId(null); + credentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE); + credentials.setCredentialsValue("testValue"); + doPost("/api/device/credentials", credentials) + .andExpect(status().isOk()); + + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + assertThat(commands).hasSize(1); + assertThat(commands.get(COAP).get(COAPS).asText()).isEqualTo(CHECK_DOCUMENTATION); + } + + @Test + @DirtiesContext + public void testDownloadMqttCert() throws Exception { + Path path = Files.createFile(Path.of("/tmp/" + PEM_CERT_FILE_NAME)); + Files.writeString(path, CERT); + + try { + String downloadedCert = doGet("/api/device-connectivity/mqtts/certificate/download", String.class); + Assert.assertEquals(CERT, downloadedCert); + } finally { + Files.deleteIfExists(path); + } + } + + @Test + @DirtiesContext + public void testDownloadMqttCertFromFileWithPrivateKey() throws Exception { + Path path = Files.createFile(Path.of("/tmp/" + PEM_CERT_FILE_NAME)); + Files.writeString(path, CERT + P_KEY); + + try { + String downloadedCert = doGet("/api/device-connectivity/mqtts/certificate/download", String.class); + Assert.assertEquals(CERT, downloadedCert); + } finally { + Files.deleteIfExists(path); + } + } + + @Test + @DirtiesContext + public void testDownloadMqttCertWithoutCertFile() throws Exception { + doGet("/api/device-connectivity/mqtts/certificate/download").andExpect(status().isNotFound()); + } + + @Test + @DirtiesContext + public void testDownloadCertWithUnknownProtocol() throws Exception { + doGet("/api/device-connectivity/unknownProtocol/certificate/download").andExpect(status().isNotFound()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java index 1c952bd549..5cc9c3dc91 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java @@ -156,9 +156,8 @@ public class DeviceControllerTest extends AbstractControllerTest { Device oldDevice = new Device(savedDevice); - testNotifyEntityOneTimeMsgToEdgeServiceNever(savedDevice, savedDevice.getId(), savedDevice.getId(), - savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED); + testNotifyEntityAllOneTime(savedDevice, savedDevice.getId(), savedDevice.getId(), savedTenant.getId(), + tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED); testNotificationUpdateGatewayNever(); Assert.assertNotNull(savedDevice); @@ -212,7 +211,7 @@ public class DeviceControllerTest extends AbstractControllerTest { Device oldDevice = new Device(savedDevice); - testNotifyEntityOneTimeMsgToEdgeServiceNever(savedDevice, savedDevice.getId(), savedDevice.getId(), + testNotifyEntityAllOneTime(savedDevice, savedDevice.getId(), savedDevice.getId(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED); testNotificationUpdateGatewayNever(); @@ -436,6 +435,9 @@ public class DeviceControllerTest extends AbstractControllerTest { @Test public void testFindDeviceTypesByTenantId() throws Exception { + DeviceProfile deviceProfile = createDeviceProfile("typeB"); + deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + List devices = new ArrayList<>(); int cntEntity = 3; @@ -446,12 +448,13 @@ public class DeviceControllerTest extends AbstractControllerTest { Device device = new Device(); device.setName("My device B" + i); device.setType("typeB"); + device.setDeviceProfileId(deviceProfile.getId()); devices.add(doPost("/api/device", device, Device.class)); } - testNotifyManyEntityManyTimeMsgToEdgeServiceNever(new Device(), new Device(), + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new Device(), new Device(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED, cntEntity); + ActionType.ADDED, cntEntity, cntEntity, cntEntity); testNotificationUpdateGatewayNever(); for (int i = 0; i < 7; i++) { @@ -491,7 +494,7 @@ public class DeviceControllerTest extends AbstractControllerTest { doDelete("/api/device/" + savedDevice.getId().getId()) .andExpect(status().isOk()); - testNotifyEntityOneTimeMsgToEdgeServiceNever(savedDevice, savedDevice.getId(), savedDevice.getId(), savedTenant.getId(), + testNotifyEntityAllOneTime(savedDevice, savedDevice.getId(), savedDevice.getId(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.DELETED, savedDevice.getId().getId().toString()); testNotificationDeleteGatewayOneTime(savedDevice); @@ -511,7 +514,7 @@ public class DeviceControllerTest extends AbstractControllerTest { Device savedDevice = doPost("/api/device", device, Device.class); Assert.assertEquals("default", savedDevice.getType()); - testNotifyEntityOneTimeMsgToEdgeServiceNever(savedDevice, savedDevice.getId(), savedDevice.getId(), + testNotifyEntityAllOneTime(savedDevice, savedDevice.getId(), savedDevice.getId(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED); testNotificationUpdateGatewayNever(); @@ -551,9 +554,9 @@ public class DeviceControllerTest extends AbstractControllerTest { + "/device/" + savedDevice.getId().getId(), Device.class); Assert.assertEquals(savedCustomer.getId(), assignedDevice.getCustomerId()); - testNotifyEntityAllOneTime(assignedDevice, assignedDevice.getId(), assignedDevice.getId(), savedTenant.getId(), + testNotifyAssignUnassignEntityAllOneTime(assignedDevice, assignedDevice.getId(), assignedDevice.getId(), savedTenant.getId(), savedCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ASSIGNED_TO_CUSTOMER, - assignedDevice.getId().getId().toString(), savedCustomer.getId().getId().toString(), + ActionType.UPDATED, assignedDevice.getId().getId().toString(), savedCustomer.getId().getId().toString(), savedCustomer.getTitle()); testNotificationUpdateGatewayNever(); @@ -566,9 +569,9 @@ public class DeviceControllerTest extends AbstractControllerTest { doDelete("/api/customer/device/" + savedDevice.getId().getId(), Device.class); Assert.assertEquals(ModelConstants.NULL_UUID, unassignedDevice.getCustomerId().getId()); - testNotifyEntityAllOneTime(unassignedDevice, unassignedDevice.getId(), unassignedDevice.getId(), savedTenant.getId(), + testNotifyAssignUnassignEntityAllOneTime(unassignedDevice, unassignedDevice.getId(), unassignedDevice.getId(), savedTenant.getId(), savedCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UNASSIGNED_FROM_CUSTOMER, - unassignedDevice.getId().getId().toString(), savedCustomer.getId().getId().toString(), + ActionType.UPDATED, unassignedDevice.getId().getId().toString(), savedCustomer.getId().getId().toString(), savedCustomer.getTitle()); testNotificationDeleteGatewayNever(); @@ -590,9 +593,9 @@ public class DeviceControllerTest extends AbstractControllerTest { Customer publicCustomer = doGet("/api/customer/" + assignedDevice.getCustomerId(), Customer.class); Assert.assertTrue(publicCustomer.isPublic()); - testNotifyEntityAllOneTime(assignedDevice, assignedDevice.getId(), assignedDevice.getId(), savedTenant.getId(), + testNotifyAssignUnassignEntityAllOneTime(assignedDevice, assignedDevice.getId(), assignedDevice.getId(), savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ASSIGNED_TO_CUSTOMER, - assignedDevice.getId().getId().toString(), publicCustomer.getId().getId().toString(), + ActionType.UPDATED, assignedDevice.getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle()); testNotificationUpdateGatewayNever(); @@ -605,9 +608,9 @@ public class DeviceControllerTest extends AbstractControllerTest { doDelete("/api/customer/device/" + savedDevice.getId().getId(), Device.class); Assert.assertEquals(ModelConstants.NULL_UUID, unassignedDevice.getCustomerId().getId()); - testNotifyEntityAllOneTime(unassignedDevice, unassignedDevice.getId(), unassignedDevice.getId(), savedTenant.getId(), + testNotifyAssignUnassignEntityAllOneTime(unassignedDevice, unassignedDevice.getId(), unassignedDevice.getId(), savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UNASSIGNED_FROM_CUSTOMER, - unassignedDevice.getId().getId().toString(), publicCustomer.getId().getId().toString(), + ActionType.UPDATED, unassignedDevice.getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle()); testNotificationDeleteGatewayNever(); @@ -842,9 +845,9 @@ public class DeviceControllerTest extends AbstractControllerTest { List devices = Futures.allAsList(futures).get(TIMEOUT, TimeUnit.SECONDS); - testNotifyManyEntityManyTimeMsgToEdgeServiceNever(new Device(), new Device(), + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new Device(), new Device(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED, cntEntity); + ActionType.ADDED, cntEntity, cntEntity, cntEntity); testNotificationUpdateGatewayNever(); List loadedDevices = new ArrayList<>(cntEntity); @@ -865,9 +868,9 @@ public class DeviceControllerTest extends AbstractControllerTest { deleteEntitiesAsync("/api/device/", loadedDevices, executor).get(TIMEOUT, TimeUnit.SECONDS); - testNotifyManyEntityManyTimeMsgToEdgeServiceNeverAdditionalInfoAny(new Device(), new Device(), + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAnyAdditionalInfoAny(new Device(), new Device(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.DELETED, cntEntity, 1); + ActionType.DELETED, ActionType.DELETED, cntEntity, cntEntity,1); testNotificationUpdateGatewayNever(); } @@ -1052,7 +1055,7 @@ public class DeviceControllerTest extends AbstractControllerTest { testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new Device(), new Device(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED, ActionType.ASSIGNED_TO_CUSTOMER, cntEntity, cntEntity, cntEntity * 2); + ActionType.ADDED, cntEntity, cntEntity, cntEntity * 2); Mockito.reset(tbClusterService, auditLogService, gatewayNotificationsService); testNotificationUpdateGatewayNever(); Mockito.reset(tbClusterService, auditLogService, gatewayNotificationsService); @@ -1074,7 +1077,7 @@ public class DeviceControllerTest extends AbstractControllerTest { testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAnyAdditionalInfoAny(new Device(), new Device(), savedTenant.getId(), customerId, tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UNASSIGNED_FROM_CUSTOMER, cntEntity, cntEntity, 3); + ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UPDATED, cntEntity, cntEntity, 3); testNotificationUpdateGatewayNever(); } diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java index d2bcfe2b88..14b052d93c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java @@ -498,7 +498,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new DeviceProfile(), new DeviceProfile(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED, ActionType.ADDED, cntEntity, cntEntity, cntEntity); + ActionType.ADDED, cntEntity, cntEntity, cntEntity); Mockito.reset(tbClusterService, auditLogService); List loadedDeviceProfiles = new ArrayList<>(); @@ -527,7 +527,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(loadedDeviceProfiles.get(0), loadedDeviceProfiles.get(0), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.DELETED, ActionType.DELETED, cntEntity, cntEntity, cntEntity, loadedDeviceProfiles.get(0).getId().getId().toString()); + ActionType.DELETED, cntEntity, cntEntity, cntEntity, loadedDeviceProfiles.get(0).getId().getId().toString()); pageLink = new PageLink(17); pageData = doGetTypedWithPageLink("/api/deviceProfiles?", diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index c8a20a38f5..081ce09ae7 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.AbstractMessage; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -46,6 +47,7 @@ import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; @@ -62,12 +64,17 @@ import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; +import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; @@ -77,6 +84,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @TestPropertySource(properties = { "edges.enabled=true", + "queue.rule-engine.stats.enabled=false" }) @ContextConfiguration(classes = {EdgeControllerTest.Config.class}) @DaoSqlTest @@ -87,10 +95,6 @@ public class EdgeControllerTest extends AbstractControllerTest { private IdComparator idComparator = new IdComparator<>(); - private Tenant savedTenant; - private TenantId tenantId; - private User tenantAdmin; - ListeningExecutorService executor; List> futures; @@ -107,35 +111,14 @@ public class EdgeControllerTest extends AbstractControllerTest { } @Before - public void beforeTest() throws Exception { + public void setupEdgeTest() throws Exception { executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); - - loginSysAdmin(); - - Tenant tenant = new Tenant(); - tenant.setTitle("My tenant for Edge"); - savedTenant = doPost("/api/tenant", tenant, Tenant.class); - tenantId = savedTenant.getId(); - Assert.assertNotNull(savedTenant); - - tenantAdmin = new User(); - tenantAdmin.setAuthority(Authority.TENANT_ADMIN); - tenantAdmin.setTenantId(savedTenant.getId()); - tenantAdmin.setEmail("tenant2@thingsboard.org"); - tenantAdmin.setFirstName("Joe"); - tenantAdmin.setLastName("Downs"); - - tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + loginTenantAdmin(); } @After - public void afterTest() throws Exception { + public void teardownEdgeTest() throws Exception { executor.shutdownNow(); - - loginSysAdmin(); - - doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) - .andExpect(status().isOk()); } @Test @@ -149,13 +132,13 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertNotNull(savedEdge); Assert.assertNotNull(savedEdge.getId()); Assert.assertTrue(savedEdge.getCreatedTime() > 0); - Assert.assertEquals(savedTenant.getId(), savedEdge.getTenantId()); + Assert.assertEquals(tenantId, savedEdge.getTenantId()); Assert.assertNotNull(savedEdge.getCustomerId()); Assert.assertEquals(NULL_UUID, savedEdge.getCustomerId().getId()); Assert.assertEquals(edge.getName(), savedEdge.getName()); testNotifyEntityBroadcastEntityStateChangeEventOneTimeMsgToEdgeServiceNever(savedEdge, savedEdge.getId(), savedEdge.getId(), - savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.ADDED); savedEdge.setName("My new edge"); @@ -165,7 +148,7 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertEquals(foundEdge.getName(), savedEdge.getName()); testNotifyEntityBroadcastEntityStateChangeEventOneTimeMsgToEdgeServiceNever(foundEdge, foundEdge.getId(), foundEdge.getId(), - savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.UPDATED); } @@ -180,8 +163,8 @@ public class EdgeControllerTest extends AbstractControllerTest { .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); - testNotifyEntityEqualsOneTimeServiceNeverError(edge, savedTenant.getId(), - tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); + testNotifyEntityEqualsOneTimeServiceNeverError(edge, tenantId, + tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); Mockito.reset(tbClusterService, auditLogService); msgError = msgErrorFieldLength("type"); @@ -191,8 +174,8 @@ public class EdgeControllerTest extends AbstractControllerTest { .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); - testNotifyEntityEqualsOneTimeServiceNeverError(edge, savedTenant.getId(), - tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); + testNotifyEntityEqualsOneTimeServiceNeverError(edge, tenantId, + tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); Mockito.reset(tbClusterService, auditLogService); msgError = msgErrorFieldLength("label"); @@ -202,8 +185,8 @@ public class EdgeControllerTest extends AbstractControllerTest { .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); - testNotifyEntityEqualsOneTimeServiceNeverError(edge, savedTenant.getId(), - tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); + testNotifyEntityEqualsOneTimeServiceNeverError(edge, tenantId, + tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); } @Test @@ -229,7 +212,7 @@ public class EdgeControllerTest extends AbstractControllerTest { } testNotifyManyEntityManyTimeMsgToEdgeServiceNeverAdditionalInfoAny(new Edge(), new Edge(), - savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.ADDED, cntEntity, 0); for (int i = 0; i < 7; i++) { @@ -262,7 +245,7 @@ public class EdgeControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); testNotifyEntityBroadcastEntityStateChangeEventOneTimeMsgToEdgeServiceNever(savedEdge, savedEdge.getId(), savedEdge.getId(), - savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.DELETED, savedEdge.getId().getId().toString()); doGet("/api/edge/" + savedEdge.getId().getId().toString()) @@ -281,8 +264,8 @@ public class EdgeControllerTest extends AbstractControllerTest { .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); - testNotifyEntityEqualsOneTimeServiceNeverError(edge, savedTenant.getId(), - tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); + testNotifyEntityEqualsOneTimeServiceNeverError(edge, tenantId, + tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); } @Test @@ -296,8 +279,8 @@ public class EdgeControllerTest extends AbstractControllerTest { .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); - testNotifyEntityEqualsOneTimeServiceNeverError(edge, savedTenant.getId(), - tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); + testNotifyEntityEqualsOneTimeServiceNeverError(edge, tenantId, + tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); } @Test @@ -316,8 +299,8 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertEquals(savedCustomer.getId(), assignedEdge.getCustomerId()); testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(assignedEdge, assignedEdge.getId(), assignedEdge.getId(), - savedTenant.getId(), savedCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ASSIGNED_TO_CUSTOMER, - assignedEdge.getId().getId().toString(), savedCustomer.getId().getId().toString(), savedCustomer.getTitle()); + tenantId, savedCustomer.getId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.ASSIGNED_TO_CUSTOMER, + ActionType.ASSIGNED_TO_CUSTOMER, assignedEdge.getId().getId().toString(), savedCustomer.getId().getId().toString(), savedCustomer.getTitle()); Edge foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); Assert.assertEquals(savedCustomer.getId(), foundEdge.getCustomerId()); @@ -327,8 +310,8 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertEquals(ModelConstants.NULL_UUID, unassignedEdge.getCustomerId().getId()); testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(unassignedEdge, unassignedEdge.getId(), unassignedEdge.getId(), - savedTenant.getId(), savedCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UNASSIGNED_FROM_CUSTOMER, - unassignedEdge.getId().getId().toString(), savedCustomer.getId().getId().toString(), savedCustomer.getTitle()); + tenantId, savedCustomer.getId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.UNASSIGNED_FROM_CUSTOMER, + ActionType.UNASSIGNED_FROM_CUSTOMER, unassignedEdge.getId().getId().toString(), savedCustomer.getId().getId().toString(), savedCustomer.getTitle()); foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); Assert.assertEquals(ModelConstants.NULL_UUID, foundEdge.getCustomerId().getId()); @@ -375,7 +358,7 @@ public class EdgeControllerTest extends AbstractControllerTest { customer.setTitle("Different customer"); Customer savedCustomer = doPost("/api/customer", customer, Customer.class); - login(tenantAdmin.getEmail(), "testPassword1"); + loginTenantAdmin(); Edge edge = constructEdge("My edge", "default"); Edge savedEdge = doPost("/api/edge", edge, Edge.class); @@ -625,8 +608,8 @@ public class EdgeControllerTest extends AbstractControllerTest { List edges = new ArrayList<>(Futures.allAsList(futures).get(TIMEOUT, TimeUnit.SECONDS)); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new Edge(), new Edge(), - savedTenant.getId(), customerId, tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ASSIGNED_TO_CUSTOMER, ActionType.ASSIGNED_TO_CUSTOMER, cntEntity, cntEntity, cntEntity * 2, + tenantId, customerId, tenantAdminUser.getId(), tenantAdminUser.getEmail(), + ActionType.ASSIGNED_TO_CUSTOMER, cntEntity, cntEntity, cntEntity * 2, new String(), new String(), new String()); List loadedEdges = new ArrayList<>(); @@ -731,7 +714,7 @@ public class EdgeControllerTest extends AbstractControllerTest { cntEntity = loadedEdgesTitle1.size(); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAnyAdditionalInfoAny(new Edge(), new Edge(), - savedTenant.getId(), customerId, tenantAdmin.getId(), tenantAdmin.getEmail(), + tenantId, customerId, tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UNASSIGNED_FROM_CUSTOMER, cntEntity, cntEntity, 3); pageLink = new PageLink(4, 0, title1); @@ -857,56 +840,45 @@ public class EdgeControllerTest extends AbstractControllerTest { @Test public void testSyncEdge() throws Exception { - Edge edge = doPost("/api/edge", constructEdge("Test Sync Edge", "test"), Edge.class); + Asset asset = new Asset(); + asset.setName("Test Sync Edge Asset 1"); + asset.setType("test"); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); Device device = new Device(); device.setName("Test Sync Edge Device 1"); device.setType("default"); Device savedDevice = doPost("/api/device", device, Device.class); + + Edge edge = doPost("/api/edge", constructEdge("Test Sync Edge", "test"), Edge.class); + doPost("/api/edge/" + edge.getId().getId().toString() + "/device/" + savedDevice.getId().getId().toString(), Device.class); - - Asset asset = new Asset(); - asset.setName("Test Sync Edge Asset 1"); - asset.setType("test"); - Asset savedAsset = doPost("/api/asset", asset, Asset.class); doPost("/api/edge/" + edge.getId().getId().toString() + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); EdgeImitator edgeImitator = new EdgeImitator(EDGE_HOST, EDGE_PORT, edge.getRoutingKey(), edge.getSecret()); edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); - edgeImitator.expectMessageAmount(20); + edgeImitator.expectMessageAmount(25); edgeImitator.connect(); assertThat(edgeImitator.waitForMessages()).as("await for messages on first connect").isTrue(); - assertThat(edgeImitator.findAllMessagesByType(QueueUpdateMsg.class)).as("one msg during sync process").hasSize(1); - List ruleChainUpdateMsgs = edgeImitator.findAllMessagesByType(RuleChainUpdateMsg.class); - assertThat(ruleChainUpdateMsgs).as("one msg during sync process, another from edge creation").hasSize(2); - assertThat(edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class)).as("one msg during sync process for 'default' device profile").hasSize(3); - assertThat(edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class)).as("one msg once device assigned to edge").hasSize(2); - assertThat(edgeImitator.findAllMessagesByType(AssetProfileUpdateMsg.class)).as("two msgs during sync process for 'default' and 'test' asset profiles").hasSize(4); - assertThat(edgeImitator.findAllMessagesByType(AssetUpdateMsg.class)).as("two msgs - one during sync process, and one more once asset assigned to edge").hasSize(2); - assertThat(edgeImitator.findAllMessagesByType(UserUpdateMsg.class)).as("one msg during sync process for tenant admin user").hasSize(1); - assertThat(edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class)).as("admin setting update").hasSize(4); - assertThat(edgeImitator.findAllMessagesByType(CustomerUpdateMsg.class)).as("one msg during sync process for 'Public' customer").hasSize(1); - verifyRuleChainMsgsAreRoot(ruleChainUpdateMsgs); - - edgeImitator.expectMessageAmount(15); + verifyFetchersMsgs(edgeImitator); + // verify queue msgs + Assert.assertTrue(popRuleChainMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, "Edge Root Rule Chain")); + Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); + Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1")); + Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); + Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1")); + Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); + + edgeImitator.expectMessageAmount(20); doPost("/api/edge/sync/" + edge.getId()); assertThat(edgeImitator.waitForMessages()).as("await for messages after edge sync rest api call").isTrue(); - assertThat(edgeImitator.findAllMessagesByType(QueueUpdateMsg.class)).as("queue msg").hasSize(1); - ruleChainUpdateMsgs = edgeImitator.findAllMessagesByType(RuleChainUpdateMsg.class); - assertThat(ruleChainUpdateMsgs).as("rule chain msg").hasSize(1); - assertThat(edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class)).as("device profile msg").hasSize(2); - assertThat(edgeImitator.findAllMessagesByType(AssetProfileUpdateMsg.class)).as("asset profile msg").hasSize(3); - assertThat(edgeImitator.findAllMessagesByType(AssetUpdateMsg.class)).as("asset update msg").hasSize(1); - assertThat(edgeImitator.findAllMessagesByType(UserUpdateMsg.class)).as("user update msg").hasSize(1); - assertThat(edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class)).as("admin setting update msg").hasSize(4); - assertThat(edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class)).as("asset update msg").hasSize(1); - assertThat(edgeImitator.findAllMessagesByType(CustomerUpdateMsg.class)).as("one msg during sync process for 'Public' customer").hasSize(1); - verifyRuleChainMsgsAreRoot(ruleChainUpdateMsgs); + verifyFetchersMsgs(edgeImitator); + Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); edgeImitator.allowIgnoredTypes(); try { @@ -922,23 +894,208 @@ public class EdgeControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - private void verifyRuleChainMsgsAreRoot(List ruleChainUpdateMsgs) { - for (RuleChainUpdateMsg ruleChainUpdateMsg : ruleChainUpdateMsgs) { - Assert.assertTrue(ruleChainUpdateMsg.getRoot()); + private void verifyFetchersMsgs(EdgeImitator edgeImitator) { + Assert.assertTrue(popQueueMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Main")); + Assert.assertTrue(popRuleChainMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Edge Root Rule Chain")); + Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail", true)); + Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail", false)); + Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mailTemplates", true)); + Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mailTemplates", false)); + Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); + Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); + Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); + Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); + Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); + Assert.assertTrue(popUserMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, TENANT_ADMIN_EMAIL, Authority.TENANT_ADMIN)); + Assert.assertTrue(popCustomerMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Public")); + Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); + Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1")); + Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); + Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1")); + Assert.assertTrue(popTenantMsg(edgeImitator.getDownlinkMsgs(), tenantId)); + Assert.assertTrue(popTenantProfileMsg(edgeImitator.getDownlinkMsgs(), tenantProfileId)); + Assert.assertTrue(popSyncCompletedMsg(edgeImitator.getDownlinkMsgs())); + } + + private boolean popQueueMsg(List messages, UpdateMsgType msgType, String name) { + for (AbstractMessage message : messages) { + if (message instanceof QueueUpdateMsg) { + QueueUpdateMsg queueUpdateMsg = (QueueUpdateMsg) message; + if (msgType.equals(queueUpdateMsg.getMsgType()) + && name.equals(queueUpdateMsg.getName())) { + messages.remove(message); + return true; + } + } + } + return false; + } + + private boolean popRuleChainMsg(List messages, UpdateMsgType msgType, String name) { + for (AbstractMessage message : messages) { + if (message instanceof RuleChainUpdateMsg) { + RuleChainUpdateMsg ruleChainUpdateMsg = (RuleChainUpdateMsg) message; + if (msgType.equals(ruleChainUpdateMsg.getMsgType()) + && name.equals(ruleChainUpdateMsg.getName()) + && ruleChainUpdateMsg.getRoot()) { + messages.remove(message); + return true; + } + } + } + return false; + } + + private boolean popAdminSettingsMsg(List messages, String key, boolean isSystem) { + for (AbstractMessage message : messages) { + if (message instanceof AdminSettingsUpdateMsg) { + AdminSettingsUpdateMsg adminSettingsUpdateMsg = (AdminSettingsUpdateMsg) message; + if (key.equals(adminSettingsUpdateMsg.getKey()) + && isSystem == adminSettingsUpdateMsg.getIsSystem()) { + messages.remove(message); + return true; + } + } + } + return false; + } + + private boolean popDeviceProfileMsg(List messages, UpdateMsgType msgType, String name) { + for (AbstractMessage message : messages) { + if (message instanceof DeviceProfileUpdateMsg) { + DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) message; + if (msgType.equals(deviceProfileUpdateMsg.getMsgType()) + && name.equals(deviceProfileUpdateMsg.getName())) { + messages.remove(message); + return true; + } + } + } + return false; + } + + private boolean popDeviceMsg(List messages, UpdateMsgType msgType, String name) { + for (AbstractMessage message : messages) { + if (message instanceof DeviceUpdateMsg) { + DeviceUpdateMsg deviceUpdateMsg = (DeviceUpdateMsg) message; + if (msgType.equals(deviceUpdateMsg.getMsgType()) + && name.equals(deviceUpdateMsg.getName())) { + messages.remove(message); + return true; + } + } + } + return false; + } + + private boolean popAssetProfileMsg(List messages, UpdateMsgType msgType, String name) { + for (AbstractMessage message : messages) { + if (message instanceof AssetProfileUpdateMsg) { + AssetProfileUpdateMsg assetProfileUpdateMsg = (AssetProfileUpdateMsg) message; + if (msgType.equals(assetProfileUpdateMsg.getMsgType()) + && name.equals(assetProfileUpdateMsg.getName())) { + messages.remove(message); + return true; + } + } + } + return false; + } + + private boolean popAssetMsg(List messages, UpdateMsgType msgType, String name) { + for (AbstractMessage message : messages) { + if (message instanceof AssetUpdateMsg) { + AssetUpdateMsg assetUpdateMsg = (AssetUpdateMsg) message; + if (msgType.equals(assetUpdateMsg.getMsgType()) + && name.equals(assetUpdateMsg.getName())) { + messages.remove(message); + return true; + } + } + } + return false; + } + + private boolean popUserMsg(List messages, UpdateMsgType msgType, String email, Authority authority) { + for (AbstractMessage message : messages) { + if (message instanceof UserUpdateMsg) { + UserUpdateMsg userUpdateMsg = (UserUpdateMsg) message; + if (msgType.equals(userUpdateMsg.getMsgType()) + && email.equals(userUpdateMsg.getEmail()) + && authority.name().equals(userUpdateMsg.getAuthority())) { + messages.remove(message); + return true; + } + } + } + return false; + } + + private boolean popCustomerMsg(List messages, UpdateMsgType msgType, String title) { + for (AbstractMessage message : messages) { + if (message instanceof CustomerUpdateMsg) { + CustomerUpdateMsg customerUpdateMsg = (CustomerUpdateMsg) message; + if (msgType.equals(customerUpdateMsg.getMsgType()) + && title.equals(customerUpdateMsg.getTitle())) { + messages.remove(message); + return true; + } + } + } + return false; + } + + private boolean popTenantMsg(List messages, TenantId tenantId1) { + for (AbstractMessage message : messages) { + if (message instanceof TenantUpdateMsg) { + TenantUpdateMsg tenantUpdateMsg = (TenantUpdateMsg) message; + TenantId tenantIdMsg = new TenantId(new UUID(tenantUpdateMsg.getIdMSB(), tenantUpdateMsg.getIdLSB())); + if (UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(tenantUpdateMsg.getMsgType()) + && tenantId1.equals(tenantIdMsg)) { + messages.remove(message); + return true; + } + } + } + return false; + } + + private boolean popTenantProfileMsg(List messages, TenantProfileId tenantProfileId) { + for (AbstractMessage message : messages) { + if (message instanceof TenantProfileUpdateMsg) { + TenantProfileUpdateMsg tenantProfileUpdateMsg = (TenantProfileUpdateMsg) message; + TenantProfileId tenantProfileIdMsg = new TenantProfileId(new UUID(tenantProfileUpdateMsg.getIdMSB(), tenantProfileUpdateMsg.getIdLSB())); + if (UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(tenantProfileUpdateMsg.getMsgType()) + && tenantProfileId.equals(tenantProfileIdMsg)) { + messages.remove(message); + return true; + } + } + } + return false; + } + + private boolean popSyncCompletedMsg(List messages) { + for (AbstractMessage message : messages) { + if (message instanceof SyncCompletedMsg) { + messages.remove(message); + return true; + } } + return false; } @Test public void testDeleteEdgeWithDeleteRelationsOk() throws Exception { EdgeId edgeId = savedEdge("Edge for Test WithRelationsOk").getId(); - testEntityDaoWithRelationsOk(savedTenant.getId(), edgeId, "/api/edge/" + edgeId); + testEntityDaoWithRelationsOk(tenantId, edgeId, "/api/edge/" + edgeId); } @Ignore @Test public void testDeleteEdgeExceptionWithRelationsTransactional() throws Exception { EdgeId edgeId = savedEdge("Edge for Test WithRelations Transactional Exception").getId(); - testEntityDaoWithRelationsTransactionalException(edgeDao, savedTenant.getId(), edgeId, "/api/edge/" + edgeId); + testEntityDaoWithRelationsTransactionalException(edgeDao, tenantId, edgeId, "/api/edge/" + edgeId); } private Edge savedEdge(String name) { diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeEventControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeEventControllerTest.java index 255aa2bdf1..04842df6a0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeEventControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeEventControllerTest.java @@ -27,8 +27,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; @@ -38,7 +36,6 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.edge.EdgeEventDao; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; @@ -54,18 +51,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @TestPropertySource(properties = { "edges.enabled=true", + "queue.rule-engine.stats.enabled=false" }) @Slf4j @DaoSqlTest public class EdgeEventControllerTest extends AbstractControllerTest { - private Tenant savedTenant; - private User tenantAdmin; - @Autowired private EdgeEventDao edgeEventDao; @SpyBean @@ -80,33 +74,11 @@ public class EdgeEventControllerTest extends AbstractControllerTest { @Before public void beforeTest() throws Exception { - loginSysAdmin(); - - Tenant tenant = new Tenant(); - tenant.setTitle("My tenant"); - savedTenant = doPost("/api/tenant", tenant, Tenant.class); - Assert.assertNotNull(savedTenant); - - tenantAdmin = new User(); - tenantAdmin.setAuthority(Authority.TENANT_ADMIN); - tenantAdmin.setTenantId(savedTenant.getId()); - tenantAdmin.setEmail("tenant2@thingsboard.org"); - tenantAdmin.setFirstName("Joe"); - tenantAdmin.setLastName("Downs"); - - tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); - // sleep 1 seconds to avoid CREDENTIALS updated message for the user - // user credentials is going to be stored and updated event pushed to edge notification service - // while service will be processing this event edge could be already added and additional message will be pushed - Thread.sleep(1000); + loginTenantAdmin(); } @After public void afterTest() throws Exception { - loginSysAdmin(); - - doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) - .andExpect(status().isOk()); } @Test @@ -127,19 +99,37 @@ public class EdgeEventControllerTest extends AbstractControllerTest { EntityRelation relation = new EntityRelation(savedAsset.getId(), savedDevice.getId(), EntityRelation.CONTAINS_TYPE); + awaitForNumberOfEdgeEvents(edgeId, 3); + doPost("/api/relation", relation); + awaitForNumberOfEdgeEvents(edgeId, 4); + + List edgeEvents = findEdgeEvents(edgeId); + Assert.assertTrue(popEdgeEvent(edgeEvents, EdgeEventType.RULE_CHAIN)); // root rule chain + Assert.assertTrue(popEdgeEvent(edgeEvents, EdgeEventType.DEVICE)); // TestDevice + Assert.assertTrue(popEdgeEvent(edgeEvents, EdgeEventType.ASSET)); // TestAsset + Assert.assertTrue(popEdgeEvent(edgeEvents, EdgeEventType.RELATION)); + Assert.assertTrue(edgeEvents.isEmpty()); + } + + private boolean popEdgeEvent(List edgeEvents, EdgeEventType edgeEventType) { + for (EdgeEvent edgeEvent : edgeEvents) { + if (edgeEventType.equals(edgeEvent.getType())) { + edgeEvents.remove(edgeEvent); + return true; + } + } + return false; + } + + private void awaitForNumberOfEdgeEvents(EdgeId edgeId, int expectedNumber) { Awaitility.await() .atMost(30, TimeUnit.SECONDS) .until(() -> { List edgeEvents = findEdgeEvents(edgeId); - return edgeEvents.size() == 4; + return edgeEvents.size() == expectedNumber; }); - List edgeEvents = findEdgeEvents(edgeId); - Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.RULE_CHAIN.equals(ee.getType()))); - Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.DEVICE.equals(ee.getType()))); - Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.ASSET.equals(ee.getType()))); - Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.RELATION.equals(ee.getType()))); } @Test @@ -195,7 +185,7 @@ public class EdgeEventControllerTest extends AbstractControllerTest { edgeEvent.setCreatedTime(System.currentTimeMillis()); edgeEvent.setTenantId(tenantId); edgeEvent.setAction(EdgeEventActionType.ADDED); - edgeEvent.setEntityId(tenantAdmin.getUuidId()); + edgeEvent.setEntityId(tenantAdminUser.getUuidId()); edgeEvent.setType(EdgeEventType.ALARM); try { edgeEventDao.saveAsync(edgeEvent).get(); diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java index a65769f3f1..beabcf77d1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java @@ -43,6 +43,7 @@ import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.StringUtils; @@ -170,7 +171,7 @@ public class EntityViewControllerTest extends AbstractControllerTest { testBroadcastEntityStateChangeEventTime(foundEntityView.getId(), tenantId, 1); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundEntityView, foundEntityView, tenantId, tenantAdminCustomerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, - ActionType.ADDED, ActionType.ADDED, 1, 0, 1); + ActionType.ADDED, 1, 1, 1); Mockito.reset(tbClusterService, auditLogService); savedView.setName("New test entity view"); @@ -183,7 +184,7 @@ public class EntityViewControllerTest extends AbstractControllerTest { testBroadcastEntityStateChangeEventTime(foundEntityView.getId(), tenantId, 1); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundEntityView, foundEntityView, tenantId, tenantAdminCustomerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, - ActionType.UPDATED, ActionType.UPDATED, 1, 1, 5); + ActionType.UPDATED, 1, 1, 5); doGet("/api/tenant/entityViews?entityViewName=" + name) .andExpect(status().isNotFound()) @@ -247,13 +248,13 @@ public class EntityViewControllerTest extends AbstractControllerTest { doDelete("/api/entityView/" + entityIdStr) .andExpect(status().isOk()); - testNotifyEntityBroadcastEntityStateChangeEventOneTimeMsgToEdgeServiceNever(savedView, savedView.getId(), savedView.getId(), + testNotifyEntityBroadcastEntityStateChangeEventOneTime(savedView, savedView.getId(), savedView.getId(), tenantId, view.getCustomerId(), tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED, entityIdStr); doGet("/api/entityView/" + entityIdStr) .andExpect(status().isNotFound()) - .andExpect(statusReason(containsString(msgErrorNoFound("Entity view",entityIdStr)))); + .andExpect(statusReason(containsString(msgErrorNoFound(EntityType.ENTITY_VIEW.getNormalName(), entityIdStr)))); } @Test @@ -286,7 +287,7 @@ public class EntityViewControllerTest extends AbstractControllerTest { testBroadcastEntityStateChangeEventTime(savedView.getId(), tenantId, 1); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(savedView, savedView, tenantId, tenantAdminCustomerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, - ActionType.UPDATED, ActionType.UPDATED, 1, 1, 5); + ActionType.UPDATED, 1, 1, 5); Mockito.reset(tbClusterService, auditLogService); EntityView assignedView = doPost( @@ -298,9 +299,9 @@ public class EntityViewControllerTest extends AbstractControllerTest { assertEquals(savedCustomer.getId(), foundView.getCustomerId()); testBroadcastEntityStateChangeEventNever(foundView.getId()); - testNotifyEntityAllOneTime(foundView, foundView.getId(), foundView.getId(), + testNotifyAssignUnassignEntityAllOneTime(foundView, foundView.getId(), foundView.getId(), tenantId, foundView.getCustomerId(), tenantAdminUserId, TENANT_ADMIN_EMAIL, - ActionType.ASSIGNED_TO_CUSTOMER, + ActionType.ASSIGNED_TO_CUSTOMER, ActionType.UPDATED, foundView.getId().getId().toString(), foundView.getCustomerId().getId().toString(), savedCustomer.getTitle()); EntityView unAssignedView = doDelete("/api/customer/entityView/" + savedView.getId().getId().toString(), EntityView.class); @@ -310,9 +311,9 @@ public class EntityViewControllerTest extends AbstractControllerTest { assertEquals(ModelConstants.NULL_UUID, foundView.getCustomerId().getId()); testBroadcastEntityStateChangeEventNever(foundView.getId()); - testNotifyEntityAllOneTime(unAssignedView, savedView.getId(), savedView.getId(), + testNotifyAssignUnassignEntityAllOneTime(unAssignedView, savedView.getId(), savedView.getId(), tenantId, savedView.getCustomerId(), tenantAdminUserId, TENANT_ADMIN_EMAIL, - ActionType.UNASSIGNED_FROM_CUSTOMER, + ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UPDATED, assignedView.getId().getId().toString(), savedView.getCustomerId().getId().toString(), savedCustomer.getTitle()); } @@ -328,9 +329,9 @@ public class EntityViewControllerTest extends AbstractControllerTest { Assert.assertTrue(publicCustomer.isPublic()); testBroadcastEntityStateChangeEventNever(assignedView.getId()); - testNotifyEntityAllOneTime(assignedView, assignedView.getId(), assignedView.getId(), + testNotifyAssignUnassignEntityAllOneTime(assignedView, assignedView.getId(), assignedView.getId(), tenantId, assignedView.getCustomerId(), tenantAdminUserId, TENANT_ADMIN_EMAIL, - ActionType.ASSIGNED_TO_CUSTOMER, + ActionType.ASSIGNED_TO_CUSTOMER, ActionType.UPDATED, assignedView.getId().getId().toString(), assignedView.getCustomerId().getId().toString(), publicCustomer.getTitle()); EntityView foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class); @@ -343,9 +344,9 @@ public class EntityViewControllerTest extends AbstractControllerTest { assertEquals(ModelConstants.NULL_UUID, foundView.getCustomerId().getId()); testBroadcastEntityStateChangeEventNever(foundView.getId()); - testNotifyEntityAllOneTime(unAssignedView, unAssignedView.getId(), unAssignedView.getId(), + testNotifyAssignUnassignEntityAllOneTime(unAssignedView, unAssignedView.getId(), unAssignedView.getId(), tenantId, publicCustomer.getId(), tenantAdminUserId, TENANT_ADMIN_EMAIL, - ActionType.UNASSIGNED_FROM_CUSTOMER, + ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UPDATED, unAssignedView.getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle()); } @@ -425,11 +426,11 @@ public class EntityViewControllerTest extends AbstractControllerTest { testNotifyEntityBroadcastEntityStateChangeEventMany(new EntityView(), new EntityView(), tenantId, tenantAdminCustomerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, - ActionType.ADDED, ActionType.ADDED, cntEntity, 0, cntEntity*2, 0); + ActionType.ADDED, ActionType.ADDED, cntEntity, cntEntity, cntEntity * 2, 0); testNotifyEntityBroadcastEntityStateChangeEventMany(new EntityView(), new EntityView(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, - ActionType.ASSIGNED_TO_CUSTOMER, ActionType.ASSIGNED_TO_CUSTOMER, cntEntity, cntEntity, + ActionType.ASSIGNED_TO_CUSTOMER, ActionType.UPDATED, cntEntity, cntEntity, cntEntity*2, 3); } @@ -464,7 +465,7 @@ public class EntityViewControllerTest extends AbstractControllerTest { testBroadcastEntityStateChangeEventNever(loadedNamesOfView1.get(0).getId()); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAnyAdditionalInfoAny(new EntityView(), new EntityView(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, - ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UNASSIGNED_FROM_CUSTOMER, cntEntity, cntEntity, 3); + ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UPDATED, cntEntity, cntEntity, 3); PageData pageData = doGetTypedWithPageLink(urlTemplate, PAGE_DATA_ENTITY_VIEW_TYPE_REF, new PageLink(4, 0, name1)); diff --git a/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java index f4472e83be..3ebbb9ebbc 100644 --- a/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java @@ -339,7 +339,7 @@ public class OtaPackageControllerTest extends AbstractControllerTest { testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new OtaPackageInfo(), new OtaPackageInfo(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED, ActionType.ADDED, cntEntity, 0, (cntEntity*2 - startIndexSaveData)); + ActionType.ADDED, cntEntity, 0, (cntEntity*2 - startIndexSaveData)); List loadedFirmwares = new ArrayList<>(); PageLink pageLink = new PageLink(24); diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java index 59a305fe5a..cafeabcf16 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java @@ -21,7 +21,6 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; -import org.junit.jupiter.api.Assertions; import org.mockito.AdditionalAnswers; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; @@ -31,7 +30,6 @@ import org.springframework.test.context.ContextConfiguration; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbCreateAlarmNode; import org.thingsboard.rule.engine.action.TbCreateAlarmNodeConfiguration; -import org.thingsboard.rule.engine.api.TbVersionedNode; import org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode; import org.thingsboard.rule.engine.metadata.TbGetRelatedDataNodeConfiguration; import org.thingsboard.server.common.data.StringUtils; @@ -52,7 +50,6 @@ import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -63,7 +60,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest public class RuleChainControllerTest extends AbstractControllerTest { - private IdComparator idComparator = new IdComparator<>(); + private final IdComparator idComparator = new IdComparator<>(); private Tenant savedTenant; private User tenantAdmin; @@ -245,7 +242,7 @@ public class RuleChainControllerTest extends AbstractControllerTest { doDelete("/api/ruleChain/" + savedRuleChain.getId().getId().toString()) .andExpect(status().isOk()); - testNotifyEntityBroadcastEntityStateChangeEventOneTimeMsgToEdgeServiceNever(savedRuleChain, savedRuleChain.getId(), savedRuleChain.getId(), + testNotifyEntityBroadcastEntityStateChangeEventOneTime(savedRuleChain, savedRuleChain.getId(), savedRuleChain.getId(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.DELETED, savedRuleChain.getId().getId().toString()); @@ -260,14 +257,13 @@ public class RuleChainControllerTest extends AbstractControllerTest { Edge savedEdge = doPost("/api/edge", edge, Edge.class); - List edgeRuleChains = new ArrayList<>(); PageLink pageLink = new PageLink(17); PageData pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId() + "/ruleChains?", new TypeReference<>() { }, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(1, pageData.getTotalElements()); - edgeRuleChains.addAll(pageData.getData()); + List edgeRuleChains = new ArrayList<>(pageData.getData()); Mockito.reset(tbClusterService, auditLogService); @@ -284,11 +280,7 @@ public class RuleChainControllerTest extends AbstractControllerTest { testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new RuleChain(), new RuleChain(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ADDED, ActionType.ADDED, cntEntity, 0, cntEntity * 2); - testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new RuleChain(), new RuleChain(), - savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.ASSIGNED_TO_EDGE, ActionType.ASSIGNED_TO_EDGE, cntEntity, cntEntity, cntEntity * 2, - new String(), new String(), new String()); + ActionType.ADDED, cntEntity, cntEntity, cntEntity * 2); Mockito.reset(tbClusterService, auditLogService); List loadedEdgeRuleChains = new ArrayList<>(); @@ -303,8 +295,8 @@ public class RuleChainControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(edgeRuleChains, idComparator); - Collections.sort(loadedEdgeRuleChains, idComparator); + edgeRuleChains.sort(idComparator); + loadedEdgeRuleChains.sort(idComparator); Assert.assertEquals(edgeRuleChains, loadedEdgeRuleChains); diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index bb542e6cf1..8725f43be1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -232,12 +232,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { String resourceIdStr = savedResource.getId().getId().toString(); //create widget type - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTitle("My widgets bundle"); - WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class); - WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString(String.format("{ \"resources\": [{\"url\":{\"entityType\":\"TB_RESOURCE\",\"id\":\"%s\"},\"isModule\":true}]}", savedResource.getId()), JsonNode.class)); doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); diff --git a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java index 47cac1b549..f7e5f6f6f3 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java @@ -15,16 +15,28 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Assert; import org.junit.Test; +import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.List; + import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.query.EntityKeyType.TIME_SERIES; @DaoSqlTest +@TestPropertySource(properties = { + "sql.attributes.value_no_xss_validation=true", + "sql.ts.value_no_xss_validation=true" +}) public class TelemetryControllerTest extends AbstractControllerTest { @Test @@ -39,6 +51,104 @@ public class TelemetryControllerTest extends AbstractControllerTest { doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); } + @Test + public void testDeleteAllTelemetryWithLatest() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + + SingleEntityFilter filter = new SingleEntityFilter(); + filter.setSingleEntity(device.getId()); + + getWsClient().subscribeLatestUpdate(List.of(new EntityKey(TIME_SERIES, "data")), filter); + + getWsClient().registerWaitForUpdate(1); + + long startTs = System.currentTimeMillis(); + + String testBody = "{\"data\": \"value\"}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk()); + + long endTs = System.currentTimeMillis(); + + ObjectNode latest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data", ObjectNode.class); + + Assert.assertNotNull(latest); + var data = latest.get("data"); + Assert.assertNotNull(data); + + Assert.assertEquals("value", data.get(0).get("value").asText()); + + ObjectNode timeseries = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data&startTs={startTs}&endTs={endTs}", ObjectNode.class, startTs, endTs); + + Assert.assertNotNull(timeseries); + + Assert.assertEquals("value", timeseries.get("data").get(0).get("value").asText()); + + doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete?keys=data&deleteAllDataForKeys=true", String.class); + + latest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data", ObjectNode.class); + + Assert.assertTrue(latest.get("data").get(0).get("value").isNull()); + + timeseries = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data&startTs={startTs}&endTs={endTs}", ObjectNode.class, startTs, endTs); + + Assert.assertTrue(timeseries.isEmpty()); + } + + @Test + public void testDeleteAllTelemetryWithoutLatest() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + + SingleEntityFilter filter = new SingleEntityFilter(); + filter.setSingleEntity(device.getId()); + + getWsClient().subscribeLatestUpdate(List.of(new EntityKey(TIME_SERIES, "data")), filter); + + getWsClient().registerWaitForUpdate(1); + + long startTs = System.currentTimeMillis(); + + String testBody = "{\"data\": \"value\"}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk()); + + long endTs = System.currentTimeMillis(); + + ObjectNode latest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data", ObjectNode.class); + + Assert.assertNotNull(latest); + + Assert.assertEquals("value", latest.get("data").get(0).get("value").asText()); + + ObjectNode timeseries = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data&startTs={startTs}&endTs={endTs}", ObjectNode.class, startTs, endTs); + + Assert.assertNotNull(timeseries); + + Assert.assertEquals("value", timeseries.get("data").get(0).get("value").asText()); + + doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete?keys=data&deleteAllDataForKeys=true&deleteLatest=false", String.class); + + latest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data", ObjectNode.class); + + Assert.assertEquals("value", latest.get("data").get(0).get("value").asText()); + + timeseries = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data&startTs={startTs}&endTs={endTs}", ObjectNode.class, startTs, endTs); + + Assert.assertTrue(timeseries.isEmpty()); + } + + @Test + public void testValueConstraintValidator() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + String correctRequestBody = "{\"data\": \"value\"}"; + doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", correctRequestBody, String.class, status().isOk()); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", correctRequestBody, String.class, status().isOk()); + String invalidRequestBody = "{\"data\": \"alert(document)\\\">\"}"; + doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest()); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); + } + private Device createDevice() throws Exception { String testToken = "TEST_TOKEN"; diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index a5b86fff85..4b594d55f9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -27,15 +27,20 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentMatcher; import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -50,24 +55,47 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueAdmin; +import org.thingsboard.server.queue.discovery.PartitionService; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; +import java.util.Deque; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; +import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_TOPIC; @TestPropertySource(properties = { "js.evaluator=mock", + "queue.rule-engine.topic-deletion-delay=10" }) @Slf4j @DaoSqlTest @@ -80,6 +108,13 @@ public class TenantControllerTest extends AbstractControllerTest { ListeningExecutorService executor; + @SpyBean + private PartitionService partitionService; + @SpyBean + private ActorSystemContext actorContext; + @SpyBean + private TbQueueAdmin queueAdmin; + @Before public void setUp() throws Exception { executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); @@ -87,6 +122,12 @@ public class TenantControllerTest extends AbstractControllerTest { @After public void tearDown() throws Exception { + loginSysAdmin(); + for (Queue queue : doGetTypedWithPageLink("/api/queues?serviceType=TB_RULE_ENGINE&", new TypeReference>() {}, new PageLink(100)).getData()) { + if (!queue.getName().equals(MAIN_QUEUE_NAME)) { + doDelete("/api/queues/" + queue.getId()).andExpect(status().isOk()); + } + } executor.shutdownNow(); } @@ -430,7 +471,7 @@ public class TenantControllerTest extends AbstractControllerTest { tenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration()); tenantProfile.setProfileData(tenantProfileData); tenantProfile.setIsolatedTbRuleEngine(true); - addQueueConfig(tenantProfile, DataConstants.MAIN_QUEUE_NAME); + addQueueConfig(tenantProfile, MAIN_QUEUE_NAME); addQueueConfig(tenantProfile, "Test"); tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); @@ -459,7 +500,7 @@ public class TenantControllerTest extends AbstractControllerTest { tenantProfileData2.setConfiguration(new DefaultTenantProfileConfiguration()); tenantProfile2.setProfileData(tenantProfileData2); tenantProfile2.setIsolatedTbRuleEngine(true); - addQueueConfig(tenantProfile2, DataConstants.MAIN_QUEUE_NAME); + addQueueConfig(tenantProfile2, MAIN_QUEUE_NAME); addQueueConfig(tenantProfile2, "Test"); addQueueConfig(tenantProfile2, "Test2"); tenantProfile2 = doPost("/api/tenantProfile", tenantProfile2, TenantProfile.class); @@ -520,10 +561,201 @@ public class TenantControllerTest extends AbstractControllerTest { doDelete("/api/tenant/" + tenant.getId().getId().toString()).andExpect(status().isOk()); } + @Test + public void testUpdateTenantProfileToIsolated() throws Exception { + loginSysAdmin(); + doPost("/api/queues?serviceType=TB_RULE_ENGINE", new Queue(TenantId.SYS_TENANT_ID, getQueueConfig(DataConstants.HP_QUEUE_NAME, DataConstants.HP_QUEUE_TOPIC))).andExpect(status().isOk()); + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Test profile"); + TenantProfileData tenantProfileData = new TenantProfileData(); + tenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration()); + tenantProfile.setProfileData(tenantProfileData); + tenantProfile.setIsolatedTbRuleEngine(false); + tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + createDifferentTenant(); + loginSysAdmin(); + savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); + savedDifferentTenant = doPost("/api/tenant", savedDifferentTenant, Tenant.class); + TenantId tenantId = differentTenantId; + + loginDifferentTenant(); + DeviceProfile hpQueueProfile = createDeviceProfile("HighPriority profile"); + hpQueueProfile.setDefaultQueueName(DataConstants.HP_QUEUE_NAME); + hpQueueProfile = doPost("/api/deviceProfile", hpQueueProfile, DeviceProfile.class); + Device hpQueueDevice = createDevice("HP", hpQueueProfile.getName(), "HP"); + + DeviceProfile mainQueueProfile = createDeviceProfile("Main profile"); + mainQueueProfile.setDefaultQueueName(MAIN_QUEUE_NAME); + mainQueueProfile = doPost("/api/deviceProfile", mainQueueProfile, DeviceProfile.class); + Device mainQueueDevice = createDevice("Main", mainQueueProfile.getName(), "Main"); + + verifyUsedQueueAndMessage(DataConstants.HP_QUEUE_NAME, tenantId, hpQueueDevice.getId(), DataConstants.ATTRIBUTES_UPDATED, () -> { + doPost("/api/plugins/telemetry/DEVICE/" + hpQueueDevice.getId() + "/attributes/SERVER_SCOPE", "{\"test\":123}", String.class); + }, usedTpi -> { + assertThat(usedTpi.getTopic()).isEqualTo(DataConstants.HP_QUEUE_TOPIC); + assertThat(usedTpi.getTenantId()).get().isEqualTo(TenantId.SYS_TENANT_ID); + }); + verifyUsedQueueAndMessage(MAIN_QUEUE_NAME, tenantId, mainQueueDevice.getId(), DataConstants.ATTRIBUTES_UPDATED, () -> { + doPost("/api/plugins/telemetry/DEVICE/" + mainQueueDevice.getId() + "/attributes/SERVER_SCOPE", "{\"test\":123}", String.class); + }, usedTpi -> { + assertThat(usedTpi.getTopic()).isEqualTo(MAIN_QUEUE_TOPIC); + assertThat(usedTpi.getTenantId()).get().isEqualTo(TenantId.SYS_TENANT_ID); + }); + + loginSysAdmin(); + tenantProfile.setIsolatedTbRuleEngine(true); + tenantProfile.getProfileData().setQueueConfiguration(List.of( + getQueueConfig(MAIN_QUEUE_NAME, MAIN_QUEUE_TOPIC) + )); + tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + loginDifferentTenant(); + verifyUsedQueueAndMessage(MAIN_QUEUE_NAME, tenantId, mainQueueDevice.getId(), DataConstants.ATTRIBUTES_UPDATED, () -> { + doPost("/api/plugins/telemetry/DEVICE/" + mainQueueDevice.getId() + "/attributes/SERVER_SCOPE", "{\"test\":123}", String.class); + }, usedTpi -> { + assertThat(usedTpi.getTopic()).isEqualTo(MAIN_QUEUE_TOPIC); + assertThat(usedTpi.getTenantId()).get().isEqualTo(tenantId); + }); + verifyUsedQueueAndMessage(DataConstants.HP_QUEUE_NAME, tenantId, hpQueueDevice.getId(), DataConstants.ATTRIBUTES_UPDATED, () -> { + doPost("/api/plugins/telemetry/DEVICE/" + hpQueueDevice.getId() + "/attributes/SERVER_SCOPE", "{\"test\":123}", String.class); + }, usedTpi -> { + assertThat(usedTpi.getTopic()).isEqualTo(DataConstants.HP_QUEUE_TOPIC); + assertThat(usedTpi.getTenantId()).get().isEqualTo(TenantId.SYS_TENANT_ID); + }); + + loginSysAdmin(); + tenantProfile.setIsolatedTbRuleEngine(true); + tenantProfile.getProfileData().setQueueConfiguration(List.of( + getQueueConfig(MAIN_QUEUE_NAME, MAIN_QUEUE_TOPIC), + getQueueConfig(DataConstants.HP_QUEUE_NAME, DataConstants.HP_QUEUE_TOPIC) + )); + tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + loginDifferentTenant(); + verifyUsedQueueAndMessage(DataConstants.HP_QUEUE_NAME, tenantId, hpQueueDevice.getId(), DataConstants.ATTRIBUTES_UPDATED, () -> { + doPost("/api/plugins/telemetry/DEVICE/" + hpQueueDevice.getId() + "/attributes/SERVER_SCOPE", "{\"test\":123}", String.class); + }, usedTpi -> { + assertThat(usedTpi.getTopic()).isEqualTo(DataConstants.HP_QUEUE_TOPIC); + assertThat(usedTpi.getTenantId()).get().isEqualTo(tenantId); + }); + verifyUsedQueueAndMessage(MAIN_QUEUE_NAME, tenantId, mainQueueDevice.getId(), DataConstants.ATTRIBUTES_UPDATED, () -> { + doPost("/api/plugins/telemetry/DEVICE/" + mainQueueDevice.getId() + "/attributes/SERVER_SCOPE", "{\"test\":123}", String.class); + }, usedTpi -> { + assertThat(usedTpi.getTopic()).isEqualTo(MAIN_QUEUE_TOPIC); + assertThat(usedTpi.getTenantId()).get().isEqualTo(tenantId); + }); + } + + @Test + public void testIsolatedQueueDeletion() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Test profile"); + TenantProfileData tenantProfileData = new TenantProfileData(); + tenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration()); + tenantProfile.setProfileData(tenantProfileData); + tenantProfile.setIsolatedTbRuleEngine(true); + addQueueConfig(tenantProfile, MAIN_QUEUE_NAME); + tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + createDifferentTenant(); + loginSysAdmin(); + savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); + savedDifferentTenant = doPost("/api/tenant", savedDifferentTenant, Tenant.class); + TenantId tenantId = differentTenantId; + await().atMost(10, TimeUnit.SECONDS) + .until(() -> { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId); + return !tpi.getTenantId().get().isSysTenantId(); + }); + TopicPartitionInfo tpi = new TopicPartitionInfo(MAIN_QUEUE_TOPIC, tenantId, 0, false); + String isolatedTopic = tpi.getFullTopicName(); + TbMsg expectedMsg = publishTbMsg(tenantId, tpi); + awaitTbMsg(tbMsg -> tbMsg.getId().equals(expectedMsg.getId()), 10000); // to wait for consumer start + + loginSysAdmin(); + tenantProfile.setIsolatedTbRuleEngine(false); + tenantProfile.getProfileData().setQueueConfiguration(Collections.emptyList()); + tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + await().atMost(10, TimeUnit.SECONDS) + .until(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId) + .getTenantId().get().isSysTenantId()); + + Deque submittedMsgs = new LinkedList<>(); + await().atLeast(8, TimeUnit.SECONDS) // due to topic-deletion-delay + .atMost(20, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted(() -> { + TbMsg tbMsg = publishTbMsg(tenantId, tpi); + submittedMsgs.add(tbMsg.getId()); + + verify(queueAdmin, times(1)).deleteTopic(eq(isolatedTopic)); + }); + submittedMsgs.removeLast(); + for (UUID msgId : submittedMsgs) { + verify(actorContext, timeout(2000)).tell(argThat(msg -> { + return msg instanceof QueueToRuleEngineMsg && ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(msgId); + })); + } + } + + private TbMsg publishTbMsg(TenantId tenantId, TopicPartitionInfo tpi) { + TbMsg tbMsg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", tenantId, TbMsgMetaData.EMPTY, "{\"test\":1}"); + TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setTbMsg(TbMsg.toByteString(tbMsg)).build(); + tbClusterService.pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, null); + return tbMsg; + } + + private void verifyUsedQueueAndMessage(String queue, TenantId tenantId, EntityId entityId, String msgType, Runnable action, Consumer tpiAssert) { + await().atMost(15, TimeUnit.SECONDS) + .untilAsserted(() -> { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, queue, tenantId, entityId); + tpiAssert.accept(tpi); + }); + action.run(); + TbMsg tbMsg = awaitTbMsg(msg -> msg.getOriginator().equals(entityId) + && msg.getType().equals(msgType), 10000); + assertThat(tbMsg.getQueueName()).isEqualTo(queue); + + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, queue, tenantId, entityId); + tpiAssert.accept(tpi); + } + + protected TbMsg awaitTbMsg(Predicate predicate, int timeoutMillis) { + AtomicReference tbMsgCaptor = new AtomicReference<>(); + verify(actorContext, timeout(timeoutMillis).atLeastOnce()).tell(argThat(actorMsg -> { + if (!(actorMsg instanceof QueueToRuleEngineMsg)) { + return false; + } + TbMsg tbMsg = ((QueueToRuleEngineMsg) actorMsg).getMsg(); + if (predicate.test(tbMsg)) { + tbMsgCaptor.set(tbMsg); + return true; + } + return false; + })); + return tbMsgCaptor.get(); + } + private void addQueueConfig(TenantProfile tenantProfile, String queueName) { + TenantProfileQueueConfiguration queueConfiguration = getQueueConfig(queueName, "tb_rule_engine." + queueName.toLowerCase()); + TenantProfileData profileData = tenantProfile.getProfileData(); + + List configs = profileData.getQueueConfiguration(); + if (configs == null) { + configs = new ArrayList<>(); + } + configs.add(queueConfiguration); + profileData.setQueueConfiguration(configs); + tenantProfile.setProfileData(profileData); + } + + private TenantProfileQueueConfiguration getQueueConfig(String queueName, String topic) { TenantProfileQueueConfiguration queueConfiguration = new TenantProfileQueueConfiguration(); queueConfiguration.setName(queueName); - queueConfiguration.setTopic("tb_rule_engine." + queueName.toLowerCase()); + queueConfiguration.setTopic(topic); queueConfiguration.setPollInterval(25); queueConfiguration.setPartitions(1 + new Random().nextInt(99)); queueConfiguration.setConsumerPerPartition(true); @@ -539,15 +771,7 @@ public class TenantControllerTest extends AbstractControllerTest { processingStrategy.setPauseBetweenRetries(3); processingStrategy.setMaxPauseBetweenRetries(3); queueConfiguration.setProcessingStrategy(processingStrategy); - TenantProfileData profileData = tenantProfile.getProfileData(); - - List configs = profileData.getQueueConfiguration(); - if (configs == null) { - configs = new ArrayList<>(); - } - configs.add(queueConfiguration); - profileData.setQueueConfiguration(configs); - tenantProfile.setProfileData(profileData); + return queueConfiguration; } private List getQueuesFromConfig(List queueConfiguration, List queues) { diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java index e6b37143ad..eba9fb8b12 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java @@ -167,23 +167,6 @@ public class TenantProfileControllerTest extends AbstractControllerTest { testBroadcastEntityStateChangeEventNeverTenantProfile(); } - @Test - public void testSaveSameTenantProfileWithDifferentIsolatedTbRuleEngine() throws Exception { - loginSysAdmin(); - TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); - TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); - savedTenantProfile.setIsolatedTbRuleEngine(true); - addMainQueueConfig(savedTenantProfile); - - Mockito.reset(tbClusterService); - - doPost("/api/tenantProfile", savedTenantProfile) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Can't update isolatedTbRuleEngine property"))); - - testBroadcastEntityStateChangeEventNeverTenantProfile(); - } - @Test public void testDeleteTenantProfileWithExistingTenant() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java index 67564ec898..28bb987257 100644 --- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java @@ -113,7 +113,7 @@ public class UserControllerTest extends AbstractControllerTest { testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundUser, foundUser, SYSTEM_TENANT, customerNUULId, null, SYS_ADMIN_EMAIL, - ActionType.ADDED, ActionType.ADDED, 1, 1, 1); + ActionType.ADDED, 1, 1, 1); Mockito.reset(tbClusterService, auditLogService); resetTokens(); @@ -152,7 +152,7 @@ public class UserControllerTest extends AbstractControllerTest { testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(foundUser, foundUser.getId(), foundUser.getId(), SYSTEM_TENANT, customerNUULId, null, SYS_ADMIN_EMAIL, - ActionType.DELETED, SYSTEM_TENANT.getId().toString()); + ActionType.DELETED, ActionType.DELETED, SYSTEM_TENANT.getId().toString()); } @Test @@ -397,7 +397,7 @@ public class UserControllerTest extends AbstractControllerTest { testManyUser.setTenantId(tenantId); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(testManyUser, testManyUser, SYSTEM_TENANT, customerNUULId, null, SYS_ADMIN_EMAIL, - ActionType.ADDED, ActionType.ADDED, cntEntity, cntEntity, cntEntity); + ActionType.ADDED, cntEntity, cntEntity, cntEntity); List loadedTenantAdmins = new ArrayList<>(); PageLink pageLink = new PageLink(33); @@ -510,7 +510,7 @@ public class UserControllerTest extends AbstractControllerTest { testManyUser.setTenantId(tenantId); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(testManyUser, testManyUser, SYSTEM_TENANT, customerNUULId, null, SYS_ADMIN_EMAIL, - ActionType.DELETED, ActionType.DELETED, cntEntity, NUMBER_OF_USERS, cntEntity, new String()); + ActionType.DELETED, cntEntity, NUMBER_OF_USERS, cntEntity, ""); pageLink = new PageLink(4, 0, email1); pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", diff --git a/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java index f3a6048c8f..9cc9fd0596 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; @@ -33,6 +34,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -43,7 +45,6 @@ public class WidgetTypeControllerTest extends AbstractControllerTest { private IdComparator idComparator = new IdComparator<>(); private Tenant savedTenant; - private WidgetsBundle savedWidgetsBundle; private User tenantAdmin; @Before @@ -63,10 +64,6 @@ public class WidgetTypeControllerTest extends AbstractControllerTest { tenantAdmin.setLastName("Downs"); tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); - - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTitle("My widgets bundle"); - savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class); } @After @@ -80,19 +77,17 @@ public class WidgetTypeControllerTest extends AbstractControllerTest { @Test public void testSaveWidgetType() throws Exception { WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); Assert.assertNotNull(savedWidgetType); Assert.assertNotNull(savedWidgetType.getId()); - Assert.assertNotNull(savedWidgetType.getAlias()); + Assert.assertNotNull(savedWidgetType.getFqn()); Assert.assertTrue(savedWidgetType.getCreatedTime() > 0); Assert.assertEquals(savedTenant.getId(), savedWidgetType.getTenantId()); Assert.assertEquals(widgetType.getName(), savedWidgetType.getName()); Assert.assertEquals(widgetType.getDescriptor(), savedWidgetType.getDescriptor()); - Assert.assertEquals(savedWidgetsBundle.getAlias(), savedWidgetType.getBundleAlias()); savedWidgetType.setName("New Widget Type"); @@ -105,7 +100,6 @@ public class WidgetTypeControllerTest extends AbstractControllerTest { @Test public void testUpdateWidgetTypeFromDifferentTenant() throws Exception { WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); @@ -118,7 +112,6 @@ public class WidgetTypeControllerTest extends AbstractControllerTest { @Test public void testFindWidgetTypeById() throws Exception { WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); @@ -130,7 +123,6 @@ public class WidgetTypeControllerTest extends AbstractControllerTest { @Test public void testDeleteWidgetType() throws Exception { WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); @@ -145,27 +137,15 @@ public class WidgetTypeControllerTest extends AbstractControllerTest { @Test public void testSaveWidgetTypeWithEmptyName() throws Exception { WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); doPost("/api/widgetType", widgetType) .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString("Widgets type name should be specified"))); } - @Test - public void testSaveWidgetTypeWithEmptyBundleAlias() throws Exception { - WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setName("Widget Type"); - widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); - doPost("/api/widgetType", widgetType) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Widgets type bundle alias should be specified"))); - } - @Test public void testSaveWidgetTypeWithEmptyDescriptor() throws Exception { WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{}", JsonNode.class)); doPost("/api/widgetType", widgetType) @@ -174,57 +154,37 @@ public class WidgetTypeControllerTest extends AbstractControllerTest { } @Test - public void testSaveWidgetTypeWithInvalidBundleAlias() throws Exception { + public void testUpdateWidgetTypeFqn() throws Exception { WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setBundleAlias("some_alias"); - widgetType.setName("Widget Type"); - widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); - doPost("/api/widgetType", widgetType) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Widget type is referencing to non-existent widgets bundle"))); - } - - @Test - public void testUpdateWidgetTypeBundleAlias() throws Exception { - WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); - widgetType.setName("Widget Type"); - widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); - WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); - savedWidgetType.setBundleAlias("some_alias"); - doPost("/api/widgetType", savedWidgetType) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Update of widget type bundle alias is prohibited"))); - - } - - @Test - public void testUpdateWidgetTypeAlias() throws Exception { - WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); - savedWidgetType.setAlias("some_alias"); + savedWidgetType.setFqn("some_fqn"); doPost("/api/widgetType", savedWidgetType) .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Update of widget type alias is prohibited"))); + .andExpect(statusReason(containsString("Update of widget type fqn is prohibited"))); } @Test public void testGetBundleWidgetTypes() throws Exception { + WidgetsBundle widgetsBundle = new WidgetsBundle(); + widgetsBundle.setTitle("My widgets bundle"); + widgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class); + List widgetTypes = new ArrayList<>(); for (int i=0;i<89;i++) { WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type " + i); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); widgetTypes.add(new WidgetType(doPost("/api/widgetType", widgetType, WidgetTypeDetails.class))); } - List loadedWidgetTypes = doGetTyped("/api/widgetTypes?isSystem={isSystem}&bundleAlias={bundleAlias}", - new TypeReference<>(){}, false, savedWidgetsBundle.getAlias()); + List widgetTypeIds = widgetTypes.stream().map(type -> type.getId().getId().toString()).collect(Collectors.toList()); + doPost("/api/widgetsBundle/" + widgetsBundle.getId().getId().toString() + "/widgetTypes", widgetTypeIds); + + List loadedWidgetTypes = doGetTyped("/api/widgetTypes?widgetsBundleId={widgetsBundleId}", + new TypeReference<>(){}, widgetsBundle.getId().getId().toString()); Collections.sort(widgetTypes, idComparator); Collections.sort(loadedWidgetTypes, idComparator); @@ -235,12 +195,11 @@ public class WidgetTypeControllerTest extends AbstractControllerTest { @Test public void testGetWidgetType() throws Exception { WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); - WidgetType foundWidgetType = doGet("/api/widgetType?isSystem={isSystem}&bundleAlias={bundleAlias}&alias={alias}", - WidgetType.class, false, savedWidgetsBundle.getAlias(), savedWidgetType.getAlias()); + WidgetType foundWidgetType = doGet("/api/widgetType?fqn={fqn}", + WidgetType.class, "tenant."+savedWidgetType.getFqn()); Assert.assertNotNull(foundWidgetType); Assert.assertEquals(new WidgetType(savedWidgetType), foundWidgetType); } diff --git a/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java b/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java new file mode 100644 index 0000000000..0394e8a505 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java @@ -0,0 +1,160 @@ +/** + * Copyright © 2016-2023 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.controller.plugin; + +import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.adapter.NativeWebSocketSession; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.service.ws.WebSocketSessionRef; + +import javax.websocket.RemoteEndpoint; +import javax.websocket.SendHandler; +import javax.websocket.SendResult; +import javax.websocket.Session; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@Slf4j +class TbWebSocketHandlerTest { + + TbWebSocketHandler wsHandler; + NativeWebSocketSession session; + Session nativeSession; + RemoteEndpoint.Async asyncRemote; + WebSocketSessionRef sessionRef; + int maxMsgQueuePerSession; + TbWebSocketHandler.SessionMetaData sendHandler; + ExecutorService executor; + + @BeforeEach + void setUp() throws IOException { + maxMsgQueuePerSession = 100; + executor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getClass().getSimpleName())); + wsHandler = spy(new TbWebSocketHandler()); + willDoNothing().given(wsHandler).close(any(), any()); + session = mock(NativeWebSocketSession.class); + nativeSession = mock(Session.class); + willReturn(nativeSession).given(session).getNativeSession(Session.class); + asyncRemote = mock(RemoteEndpoint.Async.class); + willReturn(asyncRemote).given(nativeSession).getAsyncRemote(); + sessionRef = mock(WebSocketSessionRef.class, Mockito.RETURNS_DEEP_STUBS); //prevent NPE on logs + sendHandler = spy(wsHandler.new SessionMetaData(session, sessionRef, maxMsgQueuePerSession)); + } + + @AfterEach + void tearDown() { + if (executor != null) { + executor.shutdownNow(); + } + } + + @Test + void sendHandler_sendMsg_parallel_no_race() throws InterruptedException { + CountDownLatch finishLatch = new CountDownLatch(maxMsgQueuePerSession * 2); + AtomicInteger sendersCount = new AtomicInteger(); + willAnswer(invocation -> { + assertThat(sendersCount.incrementAndGet()).as("no race").isEqualTo(1); + String text = invocation.getArgument(0); + SendHandler onResultHandler = invocation.getArgument(1); + SendResult sendResult = new SendResult(); + executor.submit(() -> { + sendersCount.decrementAndGet(); + onResultHandler.onResult(sendResult); + finishLatch.countDown(); + }); + return null; + }).given(asyncRemote).sendText(anyString(), any()); + + assertThat(sendHandler.isSending.get()).as("sendHandler not is in sending state").isFalse(); + //first batch + IntStream.range(0, maxMsgQueuePerSession).parallel().forEach(i -> sendHandler.sendMsg("hello " + i)); + Awaitility.await("first batch processed").atMost(30, TimeUnit.SECONDS).until(() -> finishLatch.getCount() == maxMsgQueuePerSession); + assertThat(sendHandler.isSending.get()).as("sendHandler not is in sending state").isFalse(); + //second batch - to test pause between big msg batches + IntStream.range(100, 100 + maxMsgQueuePerSession).parallel().forEach(i -> sendHandler.sendMsg("hello " + i)); + assertThat(finishLatch.await(30, TimeUnit.SECONDS)).as("all callbacks fired").isTrue(); + + verify(sendHandler, never()).closeSession(any()); + verify(sendHandler, times(maxMsgQueuePerSession * 2)).onResult(any()); + assertThat(sendHandler.isSending.get()).as("sendHandler not is in sending state").isFalse(); + } + + @Test + void sendHandler_sendMsg_message_order() throws InterruptedException { + CountDownLatch finishLatch = new CountDownLatch(maxMsgQueuePerSession); + Collection outputs = new ConcurrentLinkedQueue<>(); + willAnswer(invocation -> { + String text = invocation.getArgument(0); + outputs.add(text); + SendHandler onResultHandler = invocation.getArgument(1); + SendResult sendResult = new SendResult(); + executor.submit(() -> { + onResultHandler.onResult(sendResult); + finishLatch.countDown(); + }); + return null; + }).given(asyncRemote).sendText(anyString(), any()); + + List inputs = IntStream.range(0, maxMsgQueuePerSession).mapToObj(i -> "msg " + i).collect(Collectors.toList()); + inputs.forEach(s -> sendHandler.sendMsg(s)); + + assertThat(finishLatch.await(30, TimeUnit.SECONDS)).as("all callbacks fired").isTrue(); + assertThat(outputs).as("inputs exactly the same as outputs").containsExactlyElementsOf(inputs); + + verify(sendHandler, never()).closeSession(any()); + verify(sendHandler, times(maxMsgQueuePerSession)).onResult(any()); + } + + @Test + void sendHandler_sendMsg_queue_size_exceed() { + willDoNothing().given(asyncRemote).sendText(anyString(), any()); // send text will never call back, so queue will grow each sendMsg + sendHandler.sendMsg("first message to stay in-flight all the time during this test"); + IntStream.range(0, maxMsgQueuePerSession).parallel().forEach(i -> sendHandler.sendMsg("hello " + i)); + verify(sendHandler, never()).closeSession(any()); + sendHandler.sendMsg("excessive message"); + verify(sendHandler, times(1)).closeSession(eq(new CloseStatus(1008, "Max pending updates limit reached!"))); + verify(asyncRemote, times(1)).sendText(anyString(), any()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 82b2ce3ec2..410348762f 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -70,7 +70,6 @@ import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.edge.imitator.EdgeImitator; @@ -85,6 +84,9 @@ import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; +import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; @@ -100,16 +102,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @TestPropertySource(properties = { "edges.enabled=true", - "queue.rule-engine.stats.enabled=false", + "queue.rule-engine.stats.enabled=false" }) abstract public class AbstractEdgeTest extends AbstractControllerTest { private static final String THERMOSTAT_DEVICE_PROFILE_NAME = "Thermostat"; - protected Tenant savedTenant; - protected TenantId tenantId; - protected User tenantAdmin; - protected DeviceProfile thermostatDeviceProfile; protected EdgeImitator edgeImitator; @@ -125,32 +123,13 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { protected TbClusterService clusterService; @Before - public void beforeTest() throws Exception { - loginSysAdmin(); - - Tenant tenant = new Tenant(); - tenant.setTitle("My tenant"); - savedTenant = doPost("/api/tenant", tenant, Tenant.class); - tenantId = savedTenant.getId(); - Assert.assertNotNull(savedTenant); - - tenantAdmin = new User(); - tenantAdmin.setAuthority(Authority.TENANT_ADMIN); - tenantAdmin.setTenantId(savedTenant.getId()); - tenantAdmin.setEmail("tenant2@thingsboard.org"); - tenantAdmin.setFirstName("Joe"); - tenantAdmin.setLastName("Downs"); - - tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); - // sleep 0.5 second to avoid CREDENTIALS updated message for the user - // user credentials is going to be stored and updated event pushed to edge notification service - // while service will be processing this event edge could be already added and additional message will be pushed - Thread.sleep(500); + public void setupEdgeTest() throws Exception { + loginTenantAdmin(); installation(); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); - edgeImitator.expectMessageAmount(22); + edgeImitator.expectMessageAmount(26); edgeImitator.connect(); requestEdgeRuleChainMetadata(); @@ -181,31 +160,32 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { } @After - public void afterTest() throws Exception { + public void teardownEdgeTest() { try { - edgeImitator.disconnect(); - } catch (Exception ignored){} + edgeImitator.expectMessageAmount(2); + loginTenantAdmin(); + Assert.assertTrue(edgeImitator.waitForMessages()); - loginSysAdmin(); - - doDelete("/api/tenant/" + savedTenant.getUuidId()) - .andExpect(status().isOk()); + doDelete("/api/edge/" + edge.getId().toString()) + .andExpect(status().isOk()); + edgeImitator.disconnect(); + } catch (Exception ignored) {} } private void installation() { - edge = doPost("/api/edge", constructEdge("Test Edge", "test"), Edge.class); - thermostatDeviceProfile = this.createDeviceProfile(THERMOSTAT_DEVICE_PROFILE_NAME, createMqttDeviceProfileTransportConfiguration(new JsonTransportPayloadConfiguration(), false)); - extendDeviceProfileData(thermostatDeviceProfile); thermostatDeviceProfile = doPost("/api/deviceProfile", thermostatDeviceProfile, DeviceProfile.class); Device savedDevice = saveDevice("Edge Device 1", THERMOSTAT_DEVICE_PROFILE_NAME); - doPost("/api/edge/" + edge.getUuidId() - + "/device/" + savedDevice.getUuidId(), Device.class); Asset savedAsset = saveAsset("Edge Asset 1"); + + edge = doPost("/api/edge", constructEdge("Test Edge", "test"), Edge.class); + + doPost("/api/edge/" + edge.getUuidId() + + "/device/" + savedDevice.getUuidId(), Device.class); doPost("/api/edge/" + edge.getUuidId() + "/asset/" + savedAsset.getUuidId(), Asset.class); } @@ -244,18 +224,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { validateEdgeConfiguration(); - // 5 messages - // - 2 from device profile fetcher (default and thermostat) - // - 1 from device fetcher - // - 1 from device profile controller (thermostat) - // - 1 from device controller (thermostat) - validateDeviceProfiles(); - - // 2 messages - 1 from device fetcher and 1 from device controller - validateDevices(); - - // 2 messages - 1 from asset fetcher and 1 from asset controller - validateAssets(); + // 1 message from queue fetcher + validateQueues(); // 2 messages - 1 from rule chain fetcher and 1 from rule chain controller UUID ruleChainUUID = validateRuleChains(); @@ -266,20 +236,40 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { // 4 messages - 4 messages from fetcher - 2 from system level ('mail', 'mailTemplates') and 2 from admin level ('mail', 'mailTemplates') validateAdminSettings(); - // 3 messages + // 5 messages + // - 1 from default profile fetcher + // - 2 from device profile fetcher (default and thermostat) + // - 1 from device fetcher + // - 1 from device controller (thermostat) + validateDeviceProfiles(); + + // 4 messages + // - 1 from default profile fetcher // - 1 message from asset profile fetcher // - 1 message from asset fetcher // - 1 message from asset controller validateAssetProfiles(); - // 1 message from queue fetcher - validateQueues(); + // 2 messages - 1 from device fetcher and 1 from device controller + validateDevices(); - // 1 message from user fetcher - validateUsers(); + // 2 messages - 1 from asset fetcher and 1 from asset controller + validateAssets(); // 1 message from public customer fetcher validatePublicCustomer(); + + // 1 message from user fetcher + validateUsers(); + + // 1 from tenant fetcher + validateTenant(); + + // 1 from tenant profile fetcher + validateTenantProfile(); + + // 1 message sync completed + validateSyncCompleted(); } private void validateEdgeConfiguration() throws Exception { @@ -288,12 +278,35 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { testAutoGeneratedCodeByProtobuf(configuration); } + private void validateTenant() throws Exception { + Optional tenantUpdateMsgOpt = edgeImitator.findMessageByType(TenantUpdateMsg.class); + Assert.assertTrue(tenantUpdateMsgOpt.isPresent()); + TenantUpdateMsg tenantUpdateMsg = tenantUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantUpdateMsg.getMsgType()); + UUID tenantUUID = new UUID(tenantUpdateMsg.getIdMSB(), tenantUpdateMsg.getIdLSB()); + Tenant tenant = doGet("/api/tenant/" + tenantUUID, Tenant.class); + Assert.assertNotNull(tenant); + testAutoGeneratedCodeByProtobuf(tenantUpdateMsg); + } + + private void validateTenantProfile() throws Exception { + Optional tenantProfileUpdateMsgOpt = edgeImitator.findMessageByType(TenantProfileUpdateMsg.class); + Assert.assertTrue(tenantProfileUpdateMsgOpt.isPresent()); + TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantProfileUpdateMsg.getMsgType()); + UUID tenantProfileUUID = new UUID(tenantProfileUpdateMsg.getIdMSB(), tenantProfileUpdateMsg.getIdLSB()); + Tenant tenant = doGet("/api/tenant/" + tenantId.getId(), Tenant.class); + Assert.assertNotNull(tenant); + Assert.assertEquals(tenantProfileUUID, tenant.getTenantProfileId().getId()); + testAutoGeneratedCodeByProtobuf(tenantProfileUpdateMsg); + } + private void validateDeviceProfiles() throws Exception { List deviceProfileUpdateMsgList = edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class); - // default msg - // thermostat msg from fetcher + // default msg default device profile from fetcher + // default msg device profile from fetcher + // thermostat msg from device profile fetcher // thermostat msg from device fetcher - // thermostat msg from controller // thermostat msg from creation of device Assert.assertEquals(5, deviceProfileUpdateMsgList.size()); Optional thermostatProfileUpdateMsgOpt = @@ -394,7 +407,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { } } - private void validateMailAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) throws JsonProcessingException { + private void validateMailAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) { JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue()); Assert.assertNotNull(jsonNode.get("mailFrom")); Assert.assertNotNull(jsonNode.get("smtpProtocol")); @@ -403,7 +416,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertNotNull(jsonNode.get("timeout")); } - private void validateMailTemplatesAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) throws JsonProcessingException { + private void validateMailTemplatesAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) { JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue()); Assert.assertNotNull(jsonNode.get("accountActivated")); Assert.assertNotNull(jsonNode.get("accountLockout")); @@ -415,7 +428,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { private void validateAssetProfiles() throws Exception { List assetProfileUpdateMsgs = edgeImitator.findAllMessagesByType(AssetProfileUpdateMsg.class); - Assert.assertEquals(3, assetProfileUpdateMsgs.size()); + Assert.assertEquals(4, assetProfileUpdateMsgs.size()); AssetProfileUpdateMsg assetProfileUpdateMsg = assetProfileUpdateMsgs.get(0); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType()); UUID assetProfileUUID = new UUID(assetProfileUpdateMsg.getIdMSB(), assetProfileUpdateMsg.getIdLSB()); @@ -449,7 +462,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { UUID userUUID = new UUID(userUpdateMsg.getIdMSB(), userUpdateMsg.getIdLSB()); User user = doGet("/api/user/" + userUUID, User.class); Assert.assertNotNull(user); - Assert.assertEquals("tenant2@thingsboard.org", userUpdateMsg.getEmail()); + Assert.assertEquals("testtenant@thingsboard.org", userUpdateMsg.getEmail()); testAutoGeneratedCodeByProtobuf(userUpdateMsg); } @@ -464,6 +477,11 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertTrue(customer.isPublic()); } + private void validateSyncCompleted() { + Optional syncCompletedMsgOpt = edgeImitator.findMessageByType(SyncCompletedMsg.class); + Assert.assertTrue(syncCompletedMsgOpt.isPresent()); + } + protected Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception { // create device and assign to edge Device savedDevice = saveDevice(StringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName()); diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java index 2f5848ece6..faf0896b81 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java @@ -15,19 +15,29 @@ */ package org.thingsboard.server.edge; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -82,11 +92,11 @@ public class AssetEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedAsset.getUuidId().getMostSignificantBits(), assetUpdateMsg.getIdMSB()); Assert.assertEquals(savedAsset.getUuidId().getLeastSignificantBits(), assetUpdateMsg.getIdLSB()); - // delete asset - no messages expected + // delete asset - message expected, it was sent to all edges edgeImitator.expectMessageAmount(1); doDelete("/api/asset/" + savedAsset.getUuidId()) .andExpect(status().isOk()); - Assert.assertFalse(edgeImitator.waitForMessages(1)); + Assert.assertTrue(edgeImitator.waitForMessages(1)); // create asset #2 and assign to edge edgeImitator.expectMessageAmount(2); @@ -94,7 +104,6 @@ public class AssetEdgeTest extends AbstractEdgeTest { doPost("/api/edge/" + edge.getUuidId() + "/asset/" + savedAsset.getUuidId(), Asset.class); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); assetUpdateMsgOpt = edgeImitator.findMessageByType(AssetUpdateMsg.class); Assert.assertTrue(assetUpdateMsgOpt.isPresent()); assetUpdateMsg = assetUpdateMsgOpt.get(); @@ -155,5 +164,115 @@ public class AssetEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedAsset.getUuidId().getLeastSignificantBits(), assetUpdateMsg.getIdLSB()); } + @Test + public void testSendAssetToCloud() throws Exception { + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AssetUpdateMsg.Builder assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder(); + assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + assetUpdateMsgBuilder.setName("Asset Edge 2"); + assetUpdateMsgBuilder.setType("test"); + assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder); + uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + Asset asset = doGet("/api/asset/" + uuid, Asset.class); + Assert.assertNotNull(asset); + Assert.assertEquals("Asset Edge 2", asset.getName()); + } + + @Test + public void testSendAssetToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { + String assetOnCloudName = StringUtils.randomAlphanumeric(15); + Asset assetOnCloud = saveAsset(assetOnCloudName); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AssetUpdateMsg.Builder assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder(); + assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + assetUpdateMsgBuilder.setName(assetOnCloudName); + assetUpdateMsgBuilder.setType("test"); + assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder); + uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Optional assetUpdateMsgOpt = edgeImitator.findMessageByType(AssetUpdateMsg.class); + Assert.assertTrue(assetUpdateMsgOpt.isPresent()); + AssetUpdateMsg latestAssetUpdateMsg = assetUpdateMsgOpt.get(); + Assert.assertNotEquals(assetOnCloudName, latestAssetUpdateMsg.getName()); + + UUID newAssetId = new UUID(latestAssetUpdateMsg.getIdMSB(), latestAssetUpdateMsg.getIdLSB()); + + Assert.assertNotEquals(assetOnCloud.getUuidId(), newAssetId); + + Asset asset = doGet("/api/asset/" + newAssetId, Asset.class); + Assert.assertNotNull(asset); + Assert.assertNotEquals(assetOnCloudName, asset.getName()); + } + + @Test + public void testSendDeleteAssetOnEdgeToCloud() throws Exception { + Asset savedAsset = saveAssetOnCloudAndVerifyDeliveryToEdge(); + UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); + AssetUpdateMsg.Builder assetDeleteMsgBuilder = AssetUpdateMsg.newBuilder(); + assetDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE); + assetDeleteMsgBuilder.setIdMSB(savedAsset.getUuidId().getMostSignificantBits()); + assetDeleteMsgBuilder.setIdLSB(savedAsset.getUuidId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(assetDeleteMsgBuilder); + + upLinkMsgBuilder.addAssetUpdateMsg(assetDeleteMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + AssetInfo assetInfo = doGet("/api/asset/info/" + savedAsset.getUuidId(), AssetInfo.class); + Assert.assertNotNull(assetInfo); + List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/assets?", + new TypeReference>() { + }, new PageLink(100)).getData(); + Assert.assertFalse(edgeAssets.contains(assetInfo)); + } + + private Asset saveAssetOnCloudAndVerifyDeliveryToEdge() throws Exception { + // create asset and assign to edge + Asset savedAsset = saveAsset(StringUtils.randomAlphanumeric(15)); + edgeImitator.expectMessageAmount(1); // asset message + doPost("/api/edge/" + edge.getUuidId() + + "/asset/" + savedAsset.getUuidId(), Device.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + Optional assetUpdateMsgOpt = edgeImitator.findMessageByType(AssetUpdateMsg.class); + Assert.assertTrue(assetUpdateMsgOpt.isPresent()); + AssetUpdateMsg assetUpdateMsg = assetUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); + Assert.assertEquals(savedAsset.getUuidId().getMostSignificantBits(), assetUpdateMsg.getIdMSB()); + Assert.assertEquals(savedAsset.getUuidId().getLeastSignificantBits(), assetUpdateMsg.getIdLSB()); + return savedAsset; + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java index fd36c11db8..ebf0af1938 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java @@ -15,17 +15,24 @@ */ package org.thingsboard.server.edge; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.protobuf.AbstractMessage; import com.google.protobuf.ByteString; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -77,4 +84,100 @@ public class AssetProfileEdgeTest extends AbstractEdgeTest { unAssignFromEdgeAndDeleteRuleChain(buildingsRuleChainId); } + + @Test + public void testSendAssetProfileToCloud() throws Exception { + RuleChainId edgeRuleChainId = createEdgeRuleChainAndAssignToEdge("Asset Profile Rule Chain"); + DashboardId dashboardId = createDashboardAndAssignToEdge("Asset Profile Dashboard"); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AssetProfileUpdateMsg.Builder assetProfileUpdateMsgBuilder = AssetProfileUpdateMsg.newBuilder(); + assetProfileUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + assetProfileUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + assetProfileUpdateMsgBuilder.setName("Asset Profile On Edge"); + assetProfileUpdateMsgBuilder.setDefault(false); + assetProfileUpdateMsgBuilder.setDefaultRuleChainIdMSB(edgeRuleChainId.getId().getMostSignificantBits()); + assetProfileUpdateMsgBuilder.setDefaultRuleChainIdLSB(edgeRuleChainId.getId().getLeastSignificantBits()); + assetProfileUpdateMsgBuilder.setDefaultDashboardIdMSB(dashboardId.getId().getMostSignificantBits()); + assetProfileUpdateMsgBuilder.setDefaultDashboardIdLSB(dashboardId.getId().getLeastSignificantBits()); + assetProfileUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(assetProfileUpdateMsgBuilder); + uplinkMsgBuilder.addAssetProfileUpdateMsg(assetProfileUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + AssetProfile assetProfile = doGet("/api/assetProfile/" + uuid, AssetProfile.class); + Assert.assertNotNull(assetProfile); + Assert.assertEquals("Asset Profile On Edge", assetProfile.getName()); + Assert.assertEquals(dashboardId, assetProfile.getDefaultDashboardId()); + Assert.assertNull(assetProfile.getDefaultRuleChainId()); + Assert.assertEquals(edgeRuleChainId, assetProfile.getDefaultEdgeRuleChainId()); + + // delete profile + edgeImitator.expectMessageAmount(1); + doDelete("/api/assetProfile/" + assetProfile.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AssetProfileUpdateMsg); + AssetProfileUpdateMsg assetProfileUpdateMsg = (AssetProfileUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType()); + Assert.assertEquals(assetProfile.getUuidId().getMostSignificantBits(), assetProfileUpdateMsg.getIdMSB()); + Assert.assertEquals(assetProfile.getUuidId().getLeastSignificantBits(), assetProfileUpdateMsg.getIdLSB()); + + // cleanup + unAssignFromEdgeAndDeleteDashboard(dashboardId); + unAssignFromEdgeAndDeleteRuleChain(edgeRuleChainId); + } + + @Test + public void testSendAssetProfileToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { + String assetProfileOnCloudName = StringUtils.randomAlphanumeric(15); + + edgeImitator.expectMessageAmount(1); + AssetProfile assetProfileOnCloud = this.createAssetProfile(assetProfileOnCloudName); + assetProfileOnCloud = doPost("/api/assetProfile", assetProfileOnCloud, AssetProfile.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AssetProfileUpdateMsg.Builder assetProfileUpdateMsgBuilder = AssetProfileUpdateMsg.newBuilder(); + assetProfileUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + assetProfileUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + assetProfileUpdateMsgBuilder.setName(assetProfileOnCloudName); + assetProfileUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + uplinkMsgBuilder.addAssetProfileUpdateMsg(assetProfileUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Optional assetProfileUpdateMsgOpt = edgeImitator.findMessageByType(AssetProfileUpdateMsg.class); + Assert.assertTrue(assetProfileUpdateMsgOpt.isPresent()); + AssetProfileUpdateMsg latestAssetProfileUpdateMsg = assetProfileUpdateMsgOpt.get(); + Assert.assertNotEquals(assetProfileOnCloudName, latestAssetProfileUpdateMsg.getName()); + + Assert.assertNotEquals(assetProfileOnCloud.getUuidId(), uuid); + + AssetProfile assetProfile = doGet("/api/assetProfile/" + uuid, AssetProfile.class); + Assert.assertNotNull(assetProfile); + Assert.assertNotEquals(assetProfileOnCloudName, assetProfile.getName()); + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/CustomerEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CustomerEdgeTest.java index b9bf41fde6..251b4b4caf 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CustomerEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CustomerEdgeTest.java @@ -20,12 +20,15 @@ import org.junit.Assert; import org.junit.Test; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import java.util.Optional; +import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -74,13 +77,19 @@ public class CustomerEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedCustomer.getTitle(), customerUpdateMsg.getTitle()); // delete customer - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); doDelete("/api/customer/" + savedCustomer.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof CustomerUpdateMsg); - customerUpdateMsg = (CustomerUpdateMsg) latestMessage; + edgeConfigurationOpt = edgeImitator.findMessageByType(EdgeConfiguration.class); + Assert.assertTrue(edgeConfigurationOpt.isPresent()); + edgeConfiguration = edgeConfigurationOpt.get(); + Assert.assertEquals( + new CustomerId(EntityId.NULL_UUID), + new CustomerId(new UUID(edgeConfiguration.getCustomerIdMSB(), edgeConfiguration.getCustomerIdLSB()))); + customerUpdateOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class); + Assert.assertTrue(customerUpdateOpt.isPresent()); + customerUpdateMsg = customerUpdateOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); Assert.assertEquals(customerUpdateMsg.getIdMSB(), savedCustomer.getUuidId().getMostSignificantBits()); Assert.assertEquals(customerUpdateMsg.getIdLSB(), savedCustomer.getUuidId().getLeastSignificantBits()); diff --git a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java index 988d6fdc98..2d21d30174 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.edge; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; import com.google.protobuf.AbstractMessage; import org.junit.Assert; @@ -22,25 +23,39 @@ import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.ShortCustomerInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest public class DashboardEdgeTest extends AbstractEdgeTest { + private static final int MOBILE_ORDER = 5; + private static final String IMAGE = "data:image/png;base64,iVBORw0KGgoA"; + @Test public void testDashboards() throws Exception { // create dashboard and assign to edge edgeImitator.expectMessageAmount(1); Dashboard dashboard = new Dashboard(); dashboard.setTitle("Edge Test Dashboard"); + dashboard.setMobileHide(true); + dashboard.setImage(IMAGE); + dashboard.setMobileOrder(MOBILE_ORDER); Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); doPost("/api/edge/" + edge.getUuidId() + "/dashboard/" + savedDashboard.getUuidId(), Dashboard.class); @@ -52,6 +67,9 @@ public class DashboardEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedDashboard.getUuidId().getMostSignificantBits(), dashboardUpdateMsg.getIdMSB()); Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), dashboardUpdateMsg.getIdLSB()); Assert.assertEquals(savedDashboard.getTitle(), dashboardUpdateMsg.getTitle()); + Assert.assertTrue(dashboardUpdateMsg.getMobileHide()); + Assert.assertEquals(IMAGE, dashboardUpdateMsg.getImage()); + Assert.assertEquals(MOBILE_ORDER, dashboardUpdateMsg.getMobileOrder()); testAutoGeneratedCodeByProtobuf(dashboardUpdateMsg); // update dashboard @@ -77,11 +95,11 @@ public class DashboardEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedDashboard.getUuidId().getMostSignificantBits(), dashboardUpdateMsg.getIdMSB()); Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), dashboardUpdateMsg.getIdLSB()); - // delete dashboard - no messages expected + // delete dashboard - message expected, it was sent to all edges edgeImitator.expectMessageAmount(1); doDelete("/api/dashboard/" + savedDashboard.getUuidId()) .andExpect(status().isOk()); - Assert.assertFalse(edgeImitator.waitForMessages(1)); + Assert.assertTrue(edgeImitator.waitForMessages(1)); // create dashboard #2 and assign to edge edgeImitator.expectMessageAmount(1); @@ -149,4 +167,74 @@ public class DashboardEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), dashboardUpdateMsg.getIdLSB()); } + @Test + public void testSendDashboardToCloud() throws Exception { + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DashboardUpdateMsg.Builder dashboardUpdateMsgBuilder = DashboardUpdateMsg.newBuilder(); + dashboardUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + dashboardUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + dashboardUpdateMsgBuilder.setTitle("Edge Test Dashboard"); + dashboardUpdateMsgBuilder.setConfiguration(""); + dashboardUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(dashboardUpdateMsgBuilder); + uplinkMsgBuilder.addDashboardUpdateMsg(dashboardUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + Dashboard dashboard = doGet("/api/dashboard/" + uuid, Dashboard.class); + Assert.assertNotNull(dashboard); + Assert.assertEquals("Edge Test Dashboard", dashboard.getName()); + } + + @Test + public void testSendDeleteEntityViewOnEdgeToCloud() throws Exception { + Dashboard savedDashboard = saveDashboardOnCloudAndVerifyDeliveryToEdge(); + + UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); + DashboardUpdateMsg.Builder dashboardDeleteMsgBuilder = DashboardUpdateMsg.newBuilder(); + dashboardDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE); + dashboardDeleteMsgBuilder.setIdMSB(savedDashboard.getUuidId().getMostSignificantBits()); + dashboardDeleteMsgBuilder.setIdLSB(savedDashboard.getUuidId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(dashboardDeleteMsgBuilder); + + upLinkMsgBuilder.addDashboardUpdateMsg(dashboardDeleteMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + DashboardInfo dashboardInfo = doGet("/api/dashboard/info/" + savedDashboard.getUuidId(), DashboardInfo.class); + Assert.assertNotNull(dashboardInfo); + List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/dashboards?", + new TypeReference>() { + }, new PageLink(100)).getData(); + Assert.assertFalse(edgeAssets.contains(dashboardInfo)); + } + + private Dashboard saveDashboardOnCloudAndVerifyDeliveryToEdge() throws Exception { + // create dashboard and assign to edge + Dashboard dashboard = new Dashboard(); + dashboard.setTitle(StringUtils.randomAlphanumeric(15)); + Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); + edgeImitator.expectMessageAmount(1); // dashboard message + doPost("/api/edge/" + edge.getUuidId() + + "/dashboard/" + savedDashboard.getUuidId(), Dashboard.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + Optional dashboardUpdateMsgOpt = edgeImitator.findMessageByType(DashboardUpdateMsg.class); + Assert.assertTrue(dashboardUpdateMsgOpt.isPresent()); + DashboardUpdateMsg entityViewUpdateMsg = dashboardUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); + Assert.assertEquals(savedDashboard.getUuidId().getMostSignificantBits(), entityViewUpdateMsg.getIdMSB()); + Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), entityViewUpdateMsg.getIdLSB()); + return savedDashboard; + } + } diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java index 15d5b5c809..15b15c0be0 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java @@ -83,6 +83,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest public class DeviceEdgeTest extends AbstractEdgeTest { + private static final String DEFAULT_DEVICE_TYPE = "default"; + @Test public void testDevices() throws Exception { // create device and assign to edge; update device @@ -100,15 +102,15 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedDevice.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getIdMSB()); Assert.assertEquals(savedDevice.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getIdLSB()); - // delete device - no messages expected + // delete device - message expected, message send to all edges edgeImitator.expectMessageAmount(1); doDelete("/api/device/" + savedDevice.getUuidId()) .andExpect(status().isOk()); - Assert.assertFalse(edgeImitator.waitForMessages(1)); + Assert.assertTrue(edgeImitator.waitForMessages(1)); // create device #2 and assign to edge edgeImitator.expectMessageAmount(2); - savedDevice = saveDevice("Edge Device 3", "Default"); + savedDevice = saveDevice("Edge Device 3", DEFAULT_DEVICE_TYPE); doPost("/api/edge/" + edge.getUuidId() + "/device/" + savedDevice.getUuidId(), Device.class); Assert.assertTrue(edgeImitator.waitForMessages()); @@ -265,14 +267,16 @@ public class DeviceEdgeTest extends AbstractEdgeTest { public void testDeviceReachedMaximumAllowedOnCloud() throws Exception { // update tenant profile configuration loginSysAdmin(); - TenantProfile tenantProfile = doGet("/api/tenantProfile/" + savedTenant.getTenantProfileId().getId(), TenantProfile.class); + TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class); DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration(); profileConfiguration.setMaxDevices(1); tenantProfile.getProfileData().setConfiguration(profileConfiguration); doPost("/api/tenantProfile/", tenantProfile, TenantProfile.class); + edgeImitator.expectMessageAmount(2); loginTenantAdmin(); + Assert.assertTrue(edgeImitator.waitForMessages()); UUID uuid = Uuids.timeBased(); @@ -281,7 +285,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); deviceUpdateMsgBuilder.setName("Edge Device"); - deviceUpdateMsgBuilder.setType("default"); + deviceUpdateMsgBuilder.setType(DEFAULT_DEVICE_TYPE); deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); @@ -380,8 +384,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest { "inactivityTimeout", "3600000"); sendAttributesRequestAndVerify(device, DataConstants.SHARED_SCOPE, "{\"key2\":\"value2\"}", "key2", "value2"); + sendAttributesRequestAndVerify(device, DataConstants.SERVER_SCOPE, "{\"jsonKey\":{\"nestedJsonKey\":\"nestedJsonValue\"}}", + "jsonKey", "{\"nestedJsonKey\":\"nestedJsonValue\"}"); - doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SERVER_SCOPE, "keys", "key1, inactivityTimeout"); + doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SERVER_SCOPE, "keys", "key1, inactivityTimeout, jsonKey"); doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SHARED_SCOPE, "keys", "key2"); } @@ -479,7 +485,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { @Test public void testSendDeviceToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { String deviceOnCloudName = StringUtils.randomAlphanumeric(15); - Device deviceOnCloud = saveDevice(deviceOnCloudName, "Default"); + Device deviceOnCloud = saveDevice(deviceOnCloudName, DEFAULT_DEVICE_TYPE); UUID uuid = Uuids.timeBased(); @@ -646,6 +652,9 @@ public class DeviceEdgeTest extends AbstractEdgeTest { case LONG_V: Assert.assertEquals(Long.parseLong(expectedValue), keyValueProto.getLongV()); break; + case JSON_V: + Assert.assertEquals(JacksonUtil.toJsonNode(expectedValue), JacksonUtil.toJsonNode(keyValueProto.getJsonV())); + break; default: Assert.fail("Unexpected data type: " + keyValueProto.getType()); } diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java index 9d4cc0ca15..9d48824bed 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java @@ -15,19 +15,26 @@ */ package org.thingsboard.server.edge; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ByteString; import org.junit.Assert; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.data.PowerMode; import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration; import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultCoapDeviceTypeConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration; @@ -46,12 +53,15 @@ import org.thingsboard.server.common.data.transport.snmp.config.impl.TelemetryQu import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import org.thingsboard.server.transport.AbstractTransportIntegrationTest; import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -260,6 +270,67 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { removeDeviceProfileAndDoBasicAssert(deviceProfile); } + @Test + public void testSendDeviceProfileToCloud() throws Exception { + RuleChainId ruleChainId = createEdgeRuleChainAndAssignToEdge("Device Profile Rule Chain"); + DashboardId dashboardId = createDashboardAndAssignToEdge("Device Profile Dashboard"); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceProfileUpdateMsg.Builder deviceProfileUpdateMsgBuilder = DeviceProfileUpdateMsg.newBuilder(); + deviceProfileUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + deviceProfileUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + deviceProfileUpdateMsgBuilder.setName("Device Profile On Edge"); + deviceProfileUpdateMsgBuilder.setDefault(false); + deviceProfileUpdateMsgBuilder.setType(DeviceProfileType.DEFAULT.name()); + deviceProfileUpdateMsgBuilder.setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(createProfileData()))); + deviceProfileUpdateMsgBuilder.setDefaultRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits()); + deviceProfileUpdateMsgBuilder.setDefaultRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits()); + deviceProfileUpdateMsgBuilder.setDefaultDashboardIdMSB(dashboardId.getId().getMostSignificantBits()); + deviceProfileUpdateMsgBuilder.setDefaultDashboardIdLSB(dashboardId.getId().getLeastSignificantBits()); + deviceProfileUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(deviceProfileUpdateMsgBuilder); + uplinkMsgBuilder.addDeviceProfileUpdateMsg(deviceProfileUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + DeviceProfile deviceProfile = doGet("/api/deviceProfile/" + uuid, DeviceProfile.class); + Assert.assertNotNull(deviceProfile); + Assert.assertEquals("Device Profile On Edge", deviceProfile.getName()); + + // delete profile + edgeImitator.expectMessageAmount(1); + doDelete("/api/deviceProfile/" + deviceProfile.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); + Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); + Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); + + // cleanup + unAssignFromEdgeAndDeleteDashboard(dashboardId); + unAssignFromEdgeAndDeleteRuleChain(ruleChainId); + } + + private DeviceProfileData createProfileData() { + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration()); + deviceProfileData.setTransportConfiguration(new DefaultDeviceProfileTransportConfiguration()); + deviceProfileData.setProvisionConfiguration(new DisabledDeviceProfileProvisionConfiguration("Device Secret")); + return deviceProfileData; + } private DeviceProfile createDeviceProfileAndDoBasicAssert(String deviceProfileName, DeviceProfileTransportConfiguration deviceProfileTransportConfiguration) throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile(deviceProfileName, deviceProfileTransportConfiguration); @@ -341,4 +412,47 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { transportConfiguration.setCoapDeviceTypeConfiguration(coapDeviceTypeConfiguration); return transportConfiguration; } + + @Test + public void testSendDeviceProfileToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { + String deviceProfileOnCloudName = StringUtils.randomAlphanumeric(15); + + edgeImitator.expectMessageAmount(1); + DeviceProfile deviceProfileOnCloud = this.createDeviceProfile(deviceProfileOnCloudName); + deviceProfileOnCloud = doPost("/api/deviceProfile", deviceProfileOnCloud, DeviceProfile.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceProfileUpdateMsg.Builder deviceProfileUpdateMsgBuilder = DeviceProfileUpdateMsg.newBuilder(); + deviceProfileUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + deviceProfileUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + deviceProfileUpdateMsgBuilder.setName(deviceProfileOnCloudName); + deviceProfileUpdateMsgBuilder.setType(DeviceProfileType.DEFAULT.name()); + deviceProfileUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + deviceProfileUpdateMsgBuilder.setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfileOnCloud.getProfileData()))); + uplinkMsgBuilder.addDeviceProfileUpdateMsg(deviceProfileUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Optional deviceProfileUpdateMsgOpt = edgeImitator.findMessageByType(DeviceProfileUpdateMsg.class); + Assert.assertTrue(deviceProfileUpdateMsgOpt.isPresent()); + DeviceProfileUpdateMsg latestDeviceProfileUpdateMsg = deviceProfileUpdateMsgOpt.get(); + Assert.assertNotEquals(deviceProfileOnCloudName, latestDeviceProfileUpdateMsg.getName()); + + Assert.assertNotEquals(deviceProfileOnCloud.getUuidId(), uuid); + + DeviceProfile deviceProfile = doGet("/api/deviceProfile/" + uuid, DeviceProfile.class); + Assert.assertNotNull(deviceProfile); + Assert.assertNotEquals(deviceProfileOnCloudName, deviceProfile.getName()); + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java index 234a9473fa..470fd8e576 100644 --- a/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.edge; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; import com.google.protobuf.AbstractMessage; import com.google.protobuf.InvalidProtocolBufferException; import org.junit.Assert; @@ -22,15 +24,24 @@ import org.junit.Test; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.EdgeEntityType; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; +import java.util.List; +import java.util.Optional; import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -43,11 +54,7 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { // create entity view and assign to edge edgeImitator.expectMessageAmount(1); Device device = findDeviceByName("Edge Device 1"); - EntityView entityView = new EntityView(); - entityView.setName("Edge EntityView 1"); - entityView.setType("test"); - entityView.setEntityId(device.getId()); - EntityView savedEntityView = doPost("/api/entityView", entityView, EntityView.class); + EntityView savedEntityView = saveEntityView("Edge EntityView 1", device.getId()); doPost("/api/edge/" + edge.getUuidId() + "/entityView/" + savedEntityView.getUuidId(), EntityView.class); Assert.assertTrue(edgeImitator.waitForMessages()); @@ -94,19 +101,15 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { Assert.assertEquals(entityViewUpdateMsg.getIdMSB(), savedEntityView.getUuidId().getMostSignificantBits()); Assert.assertEquals(entityViewUpdateMsg.getIdLSB(), savedEntityView.getUuidId().getLeastSignificantBits()); - // delete entity view - no messages expected + // delete entity view - message expected, it was sent to all edges edgeImitator.expectMessageAmount(1); doDelete("/api/entityView/" + savedEntityView.getUuidId()) .andExpect(status().isOk()); - Assert.assertFalse(edgeImitator.waitForMessages(1)); + Assert.assertTrue(edgeImitator.waitForMessages(1)); // create entity view #2 and assign to edge edgeImitator.expectMessageAmount(1); - entityView = new EntityView(); - entityView.setName("Edge EntityView 2"); - entityView.setType("test"); - entityView.setEntityId(device.getId()); - savedEntityView = doPost("/api/entityView", entityView, EntityView.class); + savedEntityView = saveEntityView("Edge EntityView 2", device.getId()); doPost("/api/edge/" + edge.getUuidId() + "/entityView/" + savedEntityView.getUuidId(), EntityView.class); Assert.assertTrue(edgeImitator.waitForMessages()); @@ -158,6 +161,115 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { } + @Test + public void testSendEntityViewToCloud() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + EntityViewUpdateMsg.Builder entityViewUpdateMsgBuilder = EntityViewUpdateMsg.newBuilder(); + entityViewUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + entityViewUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + entityViewUpdateMsgBuilder.setName("Edge EntityView 2"); + entityViewUpdateMsgBuilder.setType("test"); + entityViewUpdateMsgBuilder.setEntityType(EdgeEntityType.DEVICE); + entityViewUpdateMsgBuilder.setEntityIdMSB(device.getUuidId().getMostSignificantBits()); + entityViewUpdateMsgBuilder.setEntityIdLSB(device.getUuidId().getLeastSignificantBits()); + entityViewUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(entityViewUpdateMsgBuilder); + uplinkMsgBuilder.addEntityViewUpdateMsg(entityViewUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + EntityView entityView = doGet("/api/entityView/" + uuid, EntityView.class); + Assert.assertNotNull(entityView); + Assert.assertEquals("Edge EntityView 2", entityView.getName()); + } + + @Test + public void testSendEntityViewToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + + String entityViewOnCloudName = StringUtils.randomAlphanumeric(15); + EntityView entityViewOnCloud = saveEntityView(entityViewOnCloudName, device.getId()); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + EntityViewUpdateMsg.Builder entityViewUpdateMsgBuilder = EntityViewUpdateMsg.newBuilder(); + entityViewUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + entityViewUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + entityViewUpdateMsgBuilder.setName(entityViewOnCloudName); + entityViewUpdateMsgBuilder.setType("test"); + entityViewUpdateMsgBuilder.setEntityType(EdgeEntityType.DEVICE); + entityViewUpdateMsgBuilder.setEntityIdMSB(device.getUuidId().getMostSignificantBits()); + entityViewUpdateMsgBuilder.setEntityIdLSB(device.getUuidId().getLeastSignificantBits()); + entityViewUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(entityViewUpdateMsgBuilder); + uplinkMsgBuilder.addEntityViewUpdateMsg(entityViewUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Optional entityViewUpdateMsgOpt = edgeImitator.findMessageByType(EntityViewUpdateMsg.class); + Assert.assertTrue(entityViewUpdateMsgOpt.isPresent()); + EntityViewUpdateMsg latestEntityViewUpdateMsg = entityViewUpdateMsgOpt.get(); + Assert.assertNotEquals(entityViewOnCloudName, latestEntityViewUpdateMsg.getName()); + + UUID newEntityViewId = new UUID(latestEntityViewUpdateMsg.getIdMSB(), latestEntityViewUpdateMsg.getIdLSB()); + + Assert.assertNotEquals(entityViewOnCloud.getId().getId(), newEntityViewId); + + EntityView entityView = doGet("/api/entityView/" + newEntityViewId, EntityView.class); + Assert.assertNotNull(entityView); + Assert.assertNotEquals(entityViewOnCloudName, entityView.getName()); + } + + @Test + public void testSendDeleteEntityViewOnEdgeToCloud() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + EntityView savedEntityView = saveEntityViewOnCloudAndVerifyDeliveryToEdge(device); + + UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); + EntityViewUpdateMsg.Builder entityViewDeleteMsgBuilder = EntityViewUpdateMsg.newBuilder(); + entityViewDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE); + entityViewDeleteMsgBuilder.setIdMSB(savedEntityView.getUuidId().getMostSignificantBits()); + entityViewDeleteMsgBuilder.setIdLSB(savedEntityView.getUuidId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(entityViewDeleteMsgBuilder); + + upLinkMsgBuilder.addEntityViewUpdateMsg(entityViewDeleteMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + EntityViewInfo entityViewInfo = doGet("/api/entityView/info/" + savedEntityView.getUuidId(), EntityViewInfo.class); + Assert.assertNotNull(entityViewInfo); + List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/entityViews?", + new TypeReference>() { + }, new PageLink(100)).getData(); + Assert.assertFalse(edgeAssets.contains(entityViewInfo)); + } + private void verifyEntityViewUpdateMsg(EntityView entityView, Device device) throws InvalidProtocolBufferException { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); @@ -173,6 +285,28 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { testAutoGeneratedCodeByProtobuf(entityViewUpdateMsg); } + private EntityView saveEntityViewOnCloudAndVerifyDeliveryToEdge(Device device) throws Exception { + // create entity view and assign to edge + EntityView savedEntityView = saveEntityView(StringUtils.randomAlphanumeric(15), device.getId()); + edgeImitator.expectMessageAmount(1); // entity view message + doPost("/api/edge/" + edge.getUuidId() + + "/entityView/" + savedEntityView.getUuidId(), EntityView.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + Optional entityViewUpdateMsgOpt = edgeImitator.findMessageByType(EntityViewUpdateMsg.class); + Assert.assertTrue(entityViewUpdateMsgOpt.isPresent()); + EntityViewUpdateMsg entityViewUpdateMsg = entityViewUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); + Assert.assertEquals(savedEntityView.getUuidId().getMostSignificantBits(), entityViewUpdateMsg.getIdMSB()); + Assert.assertEquals(savedEntityView.getUuidId().getLeastSignificantBits(), entityViewUpdateMsg.getIdLSB()); + return savedEntityView; + } + private EntityView saveEntityView(String name, DeviceId deviceId) { + EntityView entityView = new EntityView(); + entityView.setName(name); + entityView.setType("test"); + entityView.setEntityId(deviceId); + return doPost("/api/entityView", entityView, EntityView.class); + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java index b810810225..83b2370cf0 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java @@ -19,6 +19,9 @@ import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; +import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.rule.RuleChain; @@ -33,6 +36,7 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -81,7 +85,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { edgeImitator.expectMessageAmount(1); doDelete("/api/ruleChain/" + savedRuleChain.getUuidId()) .andExpect(status().isOk()); - Assert.assertFalse(edgeImitator.waitForMessages(1)); + Assert.assertTrue(edgeImitator.waitForMessages(5)); } @Test @@ -136,24 +140,30 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { Assert.assertEquals(ruleChainId, receivedRuleChainId); } - private void createRuleChainMetadata(RuleChain ruleChain) throws Exception { + private void createRuleChainMetadata(RuleChain ruleChain) { RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); ruleChainMetaData.setRuleChainId(ruleChain.getId()); RuleNode ruleNode1 = new RuleNode(); ruleNode1.setName("name1"); - ruleNode1.setType("type1"); - ruleNode1.setConfiguration(JacksonUtil.toJsonNode("\"key1\": \"val1\"")); + ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); + ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); + TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration(); + configuration.setFetchTo(TbMsgSource.METADATA); + configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); + ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration)); RuleNode ruleNode2 = new RuleNode(); ruleNode2.setName("name2"); - ruleNode2.setType("type2"); - ruleNode2.setConfiguration(JacksonUtil.toJsonNode("\"key2\": \"val2\"")); + ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); + ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); + ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration)); RuleNode ruleNode3 = new RuleNode(); ruleNode3.setName("name3"); - ruleNode3.setType("type3"); - ruleNode3.setConfiguration(JacksonUtil.toJsonNode("\"key3\": \"val3\"")); + ruleNode3.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); + ruleNode3.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); + ruleNode3.setConfiguration(JacksonUtil.valueToTree(configuration)); List ruleNodes = new ArrayList<>(); ruleNodes.add(ruleNode1); @@ -172,11 +182,12 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { @Test public void testSetRootRuleChain() throws Exception { // create rule chain - edgeImitator.expectMessageAmount(1); RuleChain ruleChain = new RuleChain(); ruleChain.setName("Edge New Root Rule Chain"); ruleChain.setType(RuleChainType.EDGE); RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + + edgeImitator.expectMessageAmount(1); doPost("/api/edge/" + edge.getUuidId() + "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class); Assert.assertTrue(edgeImitator.waitForMessages()); @@ -211,6 +222,6 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { edgeImitator.expectMessageAmount(1); doDelete("/api/ruleChain/" + savedRuleChain.getUuidId()) .andExpect(status().isOk()); - Assert.assertFalse(edgeImitator.waitForMessages(1)); + Assert.assertTrue(edgeImitator.waitForMessages(1)); } } diff --git a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java index 0cf1c2d149..2ee267b88c 100644 --- a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java @@ -215,7 +215,7 @@ public class TelemetryEdgeTest extends AbstractEdgeTest { @Test public void testAttributesUpdatedMsg_userEntity() throws Exception { - testAttributesUpdatedMsg(tenantAdmin.getId()); + testAttributesUpdatedMsg(tenantAdminUserId); } private void testAttributesUpdatedMsg(EntityId entityId) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.java new file mode 100644 index 0000000000..95fdc9f56a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.java @@ -0,0 +1,90 @@ +/** + * Copyright © 2016-2023 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.edge; + +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +import java.util.Optional; +import java.util.UUID; + +@DaoSqlTest +public class TenantEdgeTest extends AbstractEdgeTest { + + @Test + public void testUpdateTenant() throws Exception { + loginSysAdmin(); + + // save current value into tmp to revert after test + Tenant savedTenant = doGet("/api/tenant/" + tenantId, Tenant.class); + + // updated edge tenant + savedTenant.setTitle("Updated Title for Tenant Edge Test"); + edgeImitator.expectMessageAmount(2); // expect tenant and tenant profile update msg + savedTenant = doPost("/api/tenant", savedTenant, Tenant.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + Optional tenantUpdateMsgOpt = edgeImitator.findMessageByType(TenantUpdateMsg.class); + Assert.assertTrue(tenantUpdateMsgOpt.isPresent()); + TenantUpdateMsg tenantUpdateMsg = tenantUpdateMsgOpt.get(); + Optional tenantProfileUpdateMsgOpt = edgeImitator.findMessageByType(TenantProfileUpdateMsg.class); + Assert.assertTrue(tenantProfileUpdateMsgOpt.isPresent()); + TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantUpdateMsg.getMsgType()); + Assert.assertEquals(savedTenant.getUuidId().getMostSignificantBits(), tenantUpdateMsg.getIdMSB()); + Assert.assertEquals(savedTenant.getUuidId().getLeastSignificantBits(), tenantUpdateMsg.getIdLSB()); + Assert.assertEquals(savedTenant.getTitle(), tenantUpdateMsg.getTitle()); + Assert.assertEquals("Updated Title for Tenant Edge Test", tenantUpdateMsg.getTitle()); + Assert.assertEquals(savedTenant.getTenantProfileId(), new TenantProfileId + (new UUID(tenantUpdateMsg.getProfileIdMSB(), tenantUpdateMsg.getProfileIdLSB()))); + Assert.assertEquals(savedTenant.getTenantProfileId(), new TenantProfileId + (new UUID(tenantProfileUpdateMsg.getIdMSB(), tenantProfileUpdateMsg.getIdLSB()))); + + //change tenant profile for tenant + TenantProfile tenantProfile = createTenantProfile(); + savedTenant.setTenantProfileId(tenantProfile.getId()); + edgeImitator.expectMessageAmount(2); // expect tenant and tenant profile update msg + savedTenant = doPost("/api/tenant", savedTenant, Tenant.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + tenantUpdateMsgOpt = edgeImitator.findMessageByType(TenantUpdateMsg.class); + Assert.assertTrue(tenantUpdateMsgOpt.isPresent()); + tenantUpdateMsg = tenantUpdateMsgOpt.get(); + tenantProfileUpdateMsgOpt = edgeImitator.findMessageByType(TenantProfileUpdateMsg.class); + Assert.assertTrue(tenantProfileUpdateMsgOpt.isPresent()); + tenantProfileUpdateMsg = tenantProfileUpdateMsgOpt.get(); + // tenant update + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantUpdateMsg.getMsgType()); + Assert.assertEquals(savedTenant.getUuidId().getMostSignificantBits(), tenantUpdateMsg.getIdMSB()); + Assert.assertEquals(savedTenant.getUuidId().getLeastSignificantBits(), tenantUpdateMsg.getIdLSB()); + Assert.assertEquals(savedTenant.getTitle(), tenantUpdateMsg.getTitle()); + Assert.assertEquals(savedTenant.getTenantProfileId(), new TenantProfileId + (new UUID(tenantUpdateMsg.getProfileIdMSB(), tenantUpdateMsg.getProfileIdLSB()))); + Assert.assertEquals(savedTenant.getTenantProfileId(), new TenantProfileId + (new UUID(tenantProfileUpdateMsg.getIdMSB(), tenantProfileUpdateMsg.getIdLSB()))); + } + + private TenantProfile createTenantProfile() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("TestEdge tenant profile"); + return doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + } +} diff --git a/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java new file mode 100644 index 0000000000..ad94a8ba11 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2023 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.edge; + +import com.google.protobuf.AbstractMessage; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +@DaoSqlTest +public class TenantProfileEdgeTest extends AbstractEdgeTest { + + @Test + public void testTenantProfiles() throws Exception { + loginSysAdmin(); + + // save current values into tmp to revert after test + TenantProfile edgeTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class); + + // updated edge tenant profile + edgeTenantProfile.setName("Tenant Profile Edge Test"); + edgeTenantProfile.setDescription("Updated tenant profile Edge Test"); + edgeImitator.expectMessageAmount(1); + edgeTenantProfile = doPost("/api/tenantProfile", edgeTenantProfile, TenantProfile.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof TenantProfileUpdateMsg); + TenantProfileUpdateMsg tenantProfileUpdateMsg = (TenantProfileUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantProfileUpdateMsg.getMsgType()); + Assert.assertEquals(edgeTenantProfile.getUuidId().getMostSignificantBits(), tenantProfileUpdateMsg.getIdMSB()); + Assert.assertEquals(edgeTenantProfile.getUuidId().getLeastSignificantBits(), tenantProfileUpdateMsg.getIdLSB()); + Assert.assertEquals(edgeTenantProfile.getDescription(), tenantProfileUpdateMsg.getDescription()); + Assert.assertEquals("Updated tenant profile Edge Test", tenantProfileUpdateMsg.getDescription()); + Assert.assertEquals("Tenant Profile Edge Test", tenantProfileUpdateMsg.getName()); + + loginTenantAdmin(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index f376f50ac5..d1d555f691 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -45,18 +45,18 @@ public class UserEdgeTest extends AbstractEdgeTest { @Test public void testCreateUpdateDeleteTenantUser() throws Exception { // create user - edgeImitator.expectMessageAmount(2); + edgeImitator.expectMessageAmount(3); User newTenantAdmin = new User(); newTenantAdmin.setAuthority(Authority.TENANT_ADMIN); - newTenantAdmin.setTenantId(savedTenant.getId()); + newTenantAdmin.setTenantId(tenantId); newTenantAdmin.setEmail("tenantAdmin@thingsboard.org"); newTenantAdmin.setFirstName("Boris"); newTenantAdmin.setLastName("Johnson"); User savedTenantAdmin = createUser(newTenantAdmin, "tenant"); - Assert.assertTrue(edgeImitator.waitForMessages()); // wait 2 messages - user update msg and user credentials update msg - Optional latestMessageOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); - Assert.assertTrue(latestMessageOpt.isPresent()); - UserUpdateMsg userUpdateMsg = latestMessageOpt.get(); + Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - user update msg and x2 user credentials update msgs + Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); + Assert.assertTrue(userUpdateMsgOpt.isPresent()); + UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); @@ -79,8 +79,11 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedTenantAdmin.getLastName(), userUpdateMsg.getLastName()); // update user credentials - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); login(savedTenantAdmin.getEmail(), "tenant"); + Assert.assertTrue(edgeImitator.waitForMessages()); + + edgeImitator.expectMessageAmount(1); ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); changePasswordRequest.setCurrentPassword("tenant"); changePasswordRequest.setNewPassword("newTenant"); @@ -93,9 +96,12 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userCredentialsUpdateMsg.getUserIdLSB()); Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsUpdateMsg.getPassword())); + edgeImitator.expectMessageAmount(2); + loginTenantAdmin(); + Assert.assertTrue(edgeImitator.waitForMessages()); + // delete user edgeImitator.expectMessageAmount(1); - login(tenantAdmin.getEmail(), "testPassword1"); doDelete("/api/user/" + savedTenantAdmin.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); @@ -123,19 +129,19 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertTrue(edgeImitator.waitForMessages()); // create user - edgeImitator.expectMessageAmount(2); + edgeImitator.expectMessageAmount(3); User customerUser = new User(); customerUser.setAuthority(Authority.CUSTOMER_USER); - customerUser.setTenantId(savedTenant.getId()); + customerUser.setTenantId(tenantId); customerUser.setCustomerId(savedCustomer.getId()); customerUser.setEmail("customerUser@thingsboard.org"); customerUser.setFirstName("John"); customerUser.setLastName("Edwards"); User savedCustomerUser = createUser(customerUser, "customer"); - Assert.assertTrue(edgeImitator.waitForMessages()); // wait 2 messages - user update msg and user credentials update msg - Optional latestMessageOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); - Assert.assertTrue(latestMessageOpt.isPresent()); - UserUpdateMsg userUpdateMsg = latestMessageOpt.get(); + Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - user update msg and x2 user credentials update msgs + Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); + Assert.assertTrue(userUpdateMsgOpt.isPresent()); + UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertEquals(savedCustomerUser.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); Assert.assertEquals(savedCustomerUser.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); @@ -158,8 +164,11 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedCustomerUser.getLastName(), userUpdateMsg.getLastName()); // update user credentials - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); login(savedCustomerUser.getEmail(), "customer"); + Assert.assertTrue(edgeImitator.waitForMessages()); + + edgeImitator.expectMessageAmount(1); ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); changePasswordRequest.setCurrentPassword("customer"); changePasswordRequest.setNewPassword("newCustomer"); @@ -172,9 +181,12 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedCustomerUser.getUuidId().getLeastSignificantBits(), userCredentialsUpdateMsg.getUserIdLSB()); Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsUpdateMsg.getPassword())); + edgeImitator.expectMessageAmount(2); + loginTenantAdmin(); + Assert.assertTrue(edgeImitator.waitForMessages()); + // delete user edgeImitator.expectMessageAmount(1); - login(tenantAdmin.getEmail(), "testPassword1"); doDelete("/api/user/" + savedCustomerUser.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); @@ -191,8 +203,8 @@ public class UserEdgeTest extends AbstractEdgeTest { public void testSendUserCredentialsRequestToCloud() throws Exception { UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); - userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdmin.getId().getId().getMostSignificantBits()); - userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdmin.getId().getId().getLeastSignificantBits()); + userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits()); + userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdminUserId.getId().getLeastSignificantBits()); testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder); uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build()); @@ -207,16 +219,16 @@ public class UserEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - Assert.assertEquals(tenantAdmin.getId().getId().getMostSignificantBits(), userCredentialsUpdateMsg.getUserIdMSB()); - Assert.assertEquals(tenantAdmin.getId().getId().getLeastSignificantBits(), userCredentialsUpdateMsg.getUserIdLSB()); + Assert.assertEquals(tenantAdminUserId.getId().getMostSignificantBits(), userCredentialsUpdateMsg.getUserIdMSB()); + Assert.assertEquals(tenantAdminUserId.getId().getLeastSignificantBits(), userCredentialsUpdateMsg.getUserIdLSB()); } @Test public void sendUserCredentialsRequest() throws Exception { UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); - userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdmin.getId().getId().getMostSignificantBits()); - userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdmin.getId().getId().getLeastSignificantBits()); + userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits()); + userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdminUserId.getId().getLeastSignificantBits()); testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder); uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build()); @@ -231,8 +243,8 @@ public class UserEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - Assert.assertEquals(userCredentialsUpdateMsg.getUserIdMSB(), tenantAdmin.getId().getId().getMostSignificantBits()); - Assert.assertEquals(userCredentialsUpdateMsg.getUserIdLSB(), tenantAdmin.getId().getId().getLeastSignificantBits()); + Assert.assertEquals(userCredentialsUpdateMsg.getUserIdMSB(), tenantAdminUserId.getId().getMostSignificantBits()); + Assert.assertEquals(userCredentialsUpdateMsg.getUserIdLSB(), tenantAdminUserId.getId().getLeastSignificantBits()); testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); } diff --git a/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java index bfa87b194b..63b6114072 100644 --- a/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java @@ -54,10 +54,10 @@ public class WidgetEdgeTest extends AbstractEdgeTest { edgeImitator.expectMessageAmount(1); WidgetType widgetType = new WidgetType(); widgetType.setName("Test Widget Type"); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); ObjectNode descriptor = JacksonUtil.newObjectNode(); descriptor.put("key", "value"); widgetType.setDescriptor(descriptor); + widgetType.setDeprecated(true); WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class); Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); @@ -66,8 +66,9 @@ public class WidgetEdgeTest extends AbstractEdgeTest { Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, widgetTypeUpdateMsg.getMsgType()); Assert.assertEquals(savedWidgetType.getUuidId().getMostSignificantBits(), widgetTypeUpdateMsg.getIdMSB()); Assert.assertEquals(savedWidgetType.getUuidId().getLeastSignificantBits(), widgetTypeUpdateMsg.getIdLSB()); - Assert.assertEquals(savedWidgetType.getAlias(), widgetTypeUpdateMsg.getAlias()); + Assert.assertEquals(savedWidgetType.getFqn(), widgetTypeUpdateMsg.getFqn()); Assert.assertEquals(savedWidgetType.getName(), widgetTypeUpdateMsg.getName()); + Assert.assertTrue(widgetTypeUpdateMsg.getDeprecated()); Assert.assertEquals(JacksonUtil.toJsonNode(widgetTypeUpdateMsg.getDescriptorJson()), savedWidgetType.getDescriptor()); // update widget bundle diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index 0edf070aef..9425001aac 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -47,6 +47,8 @@ import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; @@ -177,16 +179,16 @@ public class EdgeImitator { result.add(saveDownlinkMsg(adminSettingsUpdateMsg)); } } - if (downlinkMsg.getDeviceUpdateMsgCount() > 0) { - for (DeviceUpdateMsg deviceUpdateMsg : downlinkMsg.getDeviceUpdateMsgList()) { - result.add(saveDownlinkMsg(deviceUpdateMsg)); - } - } if (downlinkMsg.getDeviceProfileUpdateMsgCount() > 0) { for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : downlinkMsg.getDeviceProfileUpdateMsgList()) { result.add(saveDownlinkMsg(deviceProfileUpdateMsg)); } } + if (downlinkMsg.getDeviceUpdateMsgCount() > 0) { + for (DeviceUpdateMsg deviceUpdateMsg : downlinkMsg.getDeviceUpdateMsgList()) { + result.add(saveDownlinkMsg(deviceUpdateMsg)); + } + } if (downlinkMsg.getDeviceCredentialsUpdateMsgCount() > 0) { for (DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg : downlinkMsg.getDeviceCredentialsUpdateMsgList()) { result.add(saveDownlinkMsg(deviceCredentialsUpdateMsg)); @@ -290,9 +292,22 @@ public class EdgeImitator { result.add(saveDownlinkMsg(queueUpdateMsg)); } } + if (downlinkMsg.getTenantUpdateMsgCount() > 0) { + for (TenantUpdateMsg tenantUpdateMsg : downlinkMsg.getTenantUpdateMsgList()) { + result.add(saveDownlinkMsg(tenantUpdateMsg)); + } + } + if (downlinkMsg.getTenantProfileUpdateMsgCount() > 0) { + for (TenantProfileUpdateMsg tenantProfileUpdateMsg : downlinkMsg.getTenantProfileUpdateMsgList()) { + result.add(saveDownlinkMsg(tenantProfileUpdateMsg)); + } + } if (downlinkMsg.hasEdgeConfiguration()) { result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); } + if (downlinkMsg.hasSyncCompletedMsg()) { + result.add(saveDownlinkMsg(downlinkMsg.getSyncCompletedMsg())); + } return Futures.allAsList(result); } diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java index 25b06e7840..7bd9ec576f 100644 --- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.queue.discovery; import com.datastax.oss.driver.api.core.uuid.Uuids; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.ListUtils; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -25,24 +26,36 @@ import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.context.ApplicationEventPublisher; import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.stream.Stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @Slf4j @RunWith(MockitoJUnitRunner.class) @@ -57,7 +70,7 @@ public class HashPartitionServiceTest { private ApplicationEventPublisher applicationEventPublisher; private QueueRoutingInfoService queueRoutingInfoService; - private String hashFunctionName = "sha256"; + private String hashFunctionName = "murmur3_128"; @Before public void setup() throws Exception { @@ -74,15 +87,15 @@ public class HashPartitionServiceTest { ReflectionTestUtils.setField(clusterRoutingService, "vcTopic", "tb.vc"); ReflectionTestUtils.setField(clusterRoutingService, "vcPartitions", 10); ReflectionTestUtils.setField(clusterRoutingService, "hashFunctionName", hashFunctionName); - TransportProtos.ServiceInfo currentServer = TransportProtos.ServiceInfo.newBuilder() + ServiceInfo currentServer = ServiceInfo.newBuilder() .setServiceId("tb-core-0") .addAllServiceTypes(Collections.singletonList(ServiceType.TB_CORE.name())) .build(); // when(queueService.resolve(Mockito.any(), Mockito.anyString())).thenAnswer(i -> i.getArguments()[1]); // when(discoveryService.getServiceInfo()).thenReturn(currentServer); - List otherServers = new ArrayList<>(); + List otherServers = new ArrayList<>(); for (int i = 1; i < SERVER_COUNT; i++) { - otherServers.add(TransportProtos.ServiceInfo.newBuilder() + otherServers.add(ServiceInfo.newBuilder() .setServiceId("tb-rule-" + i) .addAllServiceTypes(Collections.singletonList(ServiceType.TB_CORE.name())) .build()); @@ -122,10 +135,10 @@ public class HashPartitionServiceTest { int queueCount = 3; int partitionCount = 3; - List services = new ArrayList<>(); + List services = new ArrayList<>(); for (int i = 0; i < serverCount; i++) { - services.add(TransportProtos.ServiceInfo.newBuilder().setServiceId("RE-" + i).build()); + services.add(ServiceInfo.newBuilder().setServiceId("RE-" + i).build()); } long start = System.currentTimeMillis(); @@ -140,7 +153,7 @@ public class HashPartitionServiceTest { for (int queueIndex = 0; queueIndex < queueCount; queueIndex++) { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, "queue" + queueIndex, tenantId); for (int partition = 0; partition < partitionCount; partition++) { - TransportProtos.ServiceInfo serviceInfo = clusterRoutingService.resolveByPartitionIdx(services, queueKey, partition); + ServiceInfo serviceInfo = clusterRoutingService.resolveByPartitionIdx(services, queueKey, partition); String serviceId = serviceInfo.getServiceId(); map.put(serviceId, map.get(serviceId) + 1); } @@ -163,4 +176,124 @@ public class HashPartitionServiceTest { Assert.assertTrue(diffPercent < maxDiffPercent); } + @Test + public void testPartitionsAssignmentWithDedicatedServers() { + int isolatedProfilesCount = 5; + int tenantsCountPerProfile = 100; + int dedicatedServerSetsCount = 3; + int serversCountPerSet = 3; + int profilesPerSet = (int) Math.ceil((double) isolatedProfilesCount / dedicatedServerSetsCount); + + List isolatedTenantProfiles = Stream.generate(() -> new TenantProfileId(UUID.randomUUID())) + .limit(isolatedProfilesCount).collect(Collectors.toList()); + Map tenants = new HashMap<>(); + for (TenantProfileId tenantProfileId : isolatedTenantProfiles) { + for (int i = 0; i < tenantsCountPerProfile; i++) { + tenants.put(new TenantId(UUID.randomUUID()), tenantProfileId); + } + } + + List queues = new ArrayList<>(); + Queue systemQueue = new Queue(); + systemQueue.setTenantId(TenantId.SYS_TENANT_ID); + systemQueue.setName("Main"); + systemQueue.setTopic(DataConstants.MAIN_QUEUE_TOPIC); + systemQueue.setPartitions(10); + systemQueue.setId(new QueueId(UUID.randomUUID())); + queues.add(systemQueue); + tenants.forEach((tenantId, profileId) -> { + Queue isolatedQueue = new Queue(); + isolatedQueue.setTenantId(tenantId); + isolatedQueue.setName("Main"); + isolatedQueue.setTopic(DataConstants.MAIN_QUEUE_TOPIC); + isolatedQueue.setPartitions(2); + isolatedQueue.setId(new QueueId(UUID.randomUUID())); + queues.add(isolatedQueue); + when(routingInfoService.getRoutingInfo(eq(tenantId))).thenReturn(new TenantRoutingInfo(tenantId, profileId, true)); + }); + when(queueRoutingInfoService.getAllQueuesRoutingInfo()).thenReturn(queues.stream() + .map(QueueRoutingInfo::new).collect(Collectors.toList())); + + List ruleEngines = new ArrayList<>(); + Map> dedicatedServers = new HashMap<>(); + int serviceId = 0; + for (int i = 0; i < serversCountPerSet; i++) { + ServiceInfo commonServer = ServiceInfo.newBuilder() + .setServiceId("tb-rule-engine-" + serviceId) + .addAllServiceTypes(List.of(ServiceType.TB_RULE_ENGINE.name())) + .build(); + ruleEngines.add(commonServer); + serviceId++; + } + for (int i = 0; i < dedicatedServerSetsCount; i++) { + List assignedProfiles = ListUtils.partition(isolatedTenantProfiles, profilesPerSet).get(i); + for (int j = 0; j < serversCountPerSet; j++) { + ServiceInfo dedicatedServer = ServiceInfo.newBuilder() + .setServiceId("tb-rule-engine-" + serviceId) + .addAllServiceTypes(List.of(ServiceType.TB_RULE_ENGINE.name())) + .addAllAssignedTenantProfiles(assignedProfiles.stream().map(UUIDBased::toString).collect(Collectors.toList())) + .build(); + ruleEngines.add(dedicatedServer); + serviceId++; + + for (TenantProfileId assignedProfileId : assignedProfiles) { + dedicatedServers.computeIfAbsent(assignedProfileId, p -> new ArrayList<>()).add(dedicatedServer); + } + } + } + + Map>> serversPartitions = new HashMap<>(); + clusterRoutingService.init(); + for (ServiceInfo ruleEngine : ruleEngines) { + List other = new ArrayList<>(ruleEngines); + other.removeIf(serviceInfo -> serviceInfo.getServiceId().equals(ruleEngine.getServiceId())); + + clusterRoutingService.recalculatePartitions(ruleEngine, other); + clusterRoutingService.myPartitions.forEach((queueKey, partitions) -> { + serversPartitions.computeIfAbsent(queueKey, k -> new HashMap<>()).put(ruleEngine, partitions); + }); + } + assertThat(serversPartitions.keySet()).containsAll(queues.stream().map(queue -> new QueueKey(ServiceType.TB_RULE_ENGINE, queue)).collect(Collectors.toList())); + + serversPartitions.forEach((queueKey, partitionsPerServer) -> { + if (queueKey.getTenantId().isSysTenantId()) { + partitionsPerServer.forEach((server, partitions) -> { + assertThat(server.getAssignedTenantProfilesCount()).as("system queues are not assigned to dedicated servers").isZero(); + }); + } else { + List responsibleServers = dedicatedServers.get(tenants.get(queueKey.getTenantId())); + partitionsPerServer.forEach((server, partitions) -> { + assertThat(server.getAssignedTenantProfilesCount()).as("isolated queues are only assigned to dedicated servers").isPositive(); + assertThat(responsibleServers).contains(server); + }); + } + + List allPartitions = partitionsPerServer.values().stream() + .flatMap(Collection::stream) + .collect(Collectors.toList()); + assertThat(allPartitions).doesNotHaveDuplicates(); + }); + } + + @Test + public void testIsManagedByCurrentServiceCheck() { + TenantProfileId isolatedProfileId = new TenantProfileId(UUID.randomUUID()); + when(discoveryService.getAssignedTenantProfiles()).thenReturn(Set.of(isolatedProfileId.getId())); // dedicated server + TenantProfileId regularProfileId = new TenantProfileId(UUID.randomUUID()); + + TenantId isolatedTenantId = new TenantId(UUID.randomUUID()); + when(routingInfoService.getRoutingInfo(eq(isolatedTenantId))).thenReturn(new TenantRoutingInfo(isolatedTenantId, isolatedProfileId, true)); + TenantId regularTenantId = new TenantId(UUID.randomUUID()); + when(routingInfoService.getRoutingInfo(eq(regularTenantId))).thenReturn(new TenantRoutingInfo(regularTenantId, regularProfileId, false)); + + assertThat(clusterRoutingService.isManagedByCurrentService(isolatedTenantId)).isTrue(); + assertThat(clusterRoutingService.isManagedByCurrentService(regularTenantId)).isFalse(); + + + when(discoveryService.getAssignedTenantProfiles()).thenReturn(Collections.emptySet()); // common server + + assertThat(clusterRoutingService.isManagedByCurrentService(isolatedTenantId)).isTrue(); + assertThat(clusterRoutingService.isManagedByCurrentService(regularTenantId)).isTrue(); + } + } diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java index 6c2e5e9c5e..b86287c5d7 100644 --- a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java @@ -27,7 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration; -import org.thingsboard.rule.engine.metadata.FetchTo; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.server.actors.ActorSystemContext; @@ -39,6 +39,8 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.event.Event; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; @@ -142,7 +144,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode1.setDebugMode(true); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); - configuration1.setFetchTo(FetchTo.METADATA); + configuration1.setFetchTo(TbMsgSource.METADATA); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); @@ -152,14 +154,14 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode2.setDebugMode(true); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); - configuration2.setFetchTo(FetchTo.METADATA); + configuration2.setFetchTo(TbMsgSource.METADATA); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2)); metaData.setFirstNodeIndex(0); - metaData.addConnectionInfo(0, 1, "Success"); + metaData.addConnectionInfo(0, 1, TbNodeConnectionType.SUCCESS); metaData = saveRuleChainMetaData(metaData); Assert.assertNotNull(metaData); @@ -179,14 +181,14 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule TbMsgCallback tbMsgCallback = Mockito.mock(TbMsgCallback.class); Mockito.when(tbMsgCallback.isMsgValid()).thenReturn(true); - TbMsg tbMsg = TbMsg.newMsg("CUSTOM", device.getId(), new TbMsgMetaData(), "{}", tbMsgCallback); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, device.getId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, tbMsgCallback); QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(savedTenant.getId(), tbMsg, null, null); // Pushing Message to the system actorSystem.tell(qMsg); Mockito.verify(tbMsgCallback, Mockito.timeout(10000)).onSuccess(); PageData eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000); - List events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); + List events = eventsPage.getData().stream().filter(filterByPostTelemetryEventType()).collect(Collectors.toList()); Assert.assertEquals(2, events.size()); EventInfo inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get(); @@ -203,7 +205,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule RuleNode lastRuleNode = metaData.getNodes().stream().filter(node -> !node.getId().equals(finalRuleChain.getFirstRuleNodeId())).findFirst().get(); eventsPage = getDebugEvents(savedTenant.getId(), lastRuleNode.getId(), 1000); - events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); + events = eventsPage.getData().stream().filter(filterByPostTelemetryEventType()).collect(Collectors.toList()); Assert.assertEquals(2, events.size()); @@ -248,7 +250,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode1.setDebugMode(true); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); - configuration1.setFetchTo(FetchTo.METADATA); + configuration1.setFetchTo(TbMsgSource.METADATA); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); @@ -265,7 +267,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule NodeConnectionInfo connection = new NodeConnectionInfo(); connection.setFromIndex(0); connection.setToIndex(1); - connection.setType("Success"); + connection.setType(TbNodeConnectionType.SUCCESS); rootMetaData.setConnections(Collections.singletonList(connection)); rootMetaData = saveRuleChainMetaData(rootMetaData); Assert.assertNotNull(rootMetaData); @@ -282,7 +284,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode2.setDebugMode(true); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); - configuration2.setFetchTo(FetchTo.METADATA); + configuration2.setFetchTo(TbMsgSource.METADATA); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); @@ -304,7 +306,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule TbMsgCallback tbMsgCallback = Mockito.mock(TbMsgCallback.class); Mockito.when(tbMsgCallback.isMsgValid()).thenReturn(true); - TbMsg tbMsg = TbMsg.newMsg("CUSTOM", device.getId(), new TbMsgMetaData(), "{}", tbMsgCallback); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, device.getId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, tbMsgCallback); QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(savedTenant.getId(), tbMsg, null, null); // Pushing Message to the system actorSystem.tell(qMsg); @@ -312,7 +314,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule Mockito.verify(tbMsgCallback, Mockito.timeout(10000)).onSuccess(); PageData eventsPage = getDebugEvents(savedTenant.getId(), rootRuleChain.getFirstRuleNodeId(), 1000); - List events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); + List events = eventsPage.getData().stream().filter(filterByPostTelemetryEventType()).collect(Collectors.toList()); Assert.assertEquals(2, events.size()); @@ -330,7 +332,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule RuleNode lastRuleNode = secondaryMetaData.getNodes().stream().filter(node -> !node.getId().equals(finalRuleChain.getFirstRuleNodeId())).findFirst().get(); eventsPage = getDebugEvents(savedTenant.getId(), lastRuleNode.getId(), 1000); - events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); + events = eventsPage.getData().stream().filter(filterByPostTelemetryEventType()).collect(Collectors.toList()); Assert.assertEquals(2, events.size()); diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java index 9753f06aff..9b05be4a05 100644 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java @@ -24,7 +24,7 @@ import org.junit.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.metadata.FetchTo; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.server.actors.ActorSystemContext; @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -97,7 +98,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac ruleNode.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode.setDebugMode(true); TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration(); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey")); ruleNode.setConfiguration(JacksonUtil.valueToTree(configuration)); @@ -139,7 +140,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac log.warn("attr updated"); TbMsgCallback tbMsgCallback = Mockito.mock(TbMsgCallback.class); Mockito.when(tbMsgCallback.isMsgValid()).thenReturn(true); - TbMsg tbMsg = TbMsg.newMsg("CUSTOM", device.getId(), new TbMsgMetaData(), "{}", tbMsgCallback); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, device.getId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, tbMsgCallback); QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(tenantId, tbMsg, null, null); // Pushing Message to the system log.warn("before tell tbMsgCallback"); @@ -147,12 +148,12 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac log.warn("awaiting tbMsgCallback"); Mockito.verify(tbMsgCallback, Mockito.timeout(TimeUnit.SECONDS.toMillis(TIMEOUT))).onSuccess(); log.warn("awaiting events"); - List events = Awaitility.await("get debug by custom event") + List events = Awaitility.await("get debug by post telemetry event") .pollInterval(10, MILLISECONDS) .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { List debugEvents = getDebugEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), 1000) - .getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); + .getData().stream().filter(filterByPostTelemetryEventType()).collect(Collectors.toList()); log.warn("filtered debug events [{}]", debugEvents.size()); debugEvents.forEach((e) -> log.warn("event: {}", e)); return debugEvents; diff --git a/application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java index f01f4fae04..a88f25d5bf 100644 --- a/application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java @@ -71,8 +71,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -; - @Slf4j @RunWith(SpringRunner.class) @ContextConfiguration(classes = DeviceProvisionServiceImpl.class) diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java index dc3cc84c72..c1f34217ac 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java @@ -24,9 +24,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -45,6 +47,8 @@ import java.util.UUID; @RunWith(MockitoJUnitRunner.class) public class RuleChainMsgConstructorTest { + private static final String RPC_CONNECTION_TYPE = "RPC"; + private RuleChainMsgConstructor constructor; private TenantId tenantId; @@ -99,19 +103,19 @@ public class RuleChainMsgConstructorTest { Assert.assertEquals("Connections count incorrect!", 13, ruleChainMetadataUpdateMsg.getConnectionsCount()); Assert.assertEquals("Rule chain connections count incorrect!", 0, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount()); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 6, "Success"), ruleChainMetadataUpdateMsg.getConnections(0)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 10, "Success"), ruleChainMetadataUpdateMsg.getConnections(1)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, "Success"), ruleChainMetadataUpdateMsg.getConnections(2)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 11, "Success"), ruleChainMetadataUpdateMsg.getConnections(3)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 11, "Success"), ruleChainMetadataUpdateMsg.getConnections(4)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 11, "Attributes Updated"), ruleChainMetadataUpdateMsg.getConnections(5)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 7, "RPC Request from Device"), ruleChainMetadataUpdateMsg.getConnections(6)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 4, "Post telemetry"), ruleChainMetadataUpdateMsg.getConnections(7)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 5, "Post attributes"), ruleChainMetadataUpdateMsg.getConnections(8)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 8, "Other"), ruleChainMetadataUpdateMsg.getConnections(9)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, "RPC Request to Device"), ruleChainMetadataUpdateMsg.getConnections(10)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 11, "Success"), ruleChainMetadataUpdateMsg.getConnections(11)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(10, 9, "RPC"), ruleChainMetadataUpdateMsg.getConnections(12)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 6, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 10, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(1)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(2)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 11, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(3)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 11, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(4)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 11, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(5)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 7, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(6)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 4, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(7)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 5, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(8)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 8, TbNodeConnectionType.OTHER), ruleChainMetadataUpdateMsg.getConnections(9)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(10)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 11, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(11)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(10, 9, RPC_CONNECTION_TYPE), ruleChainMetadataUpdateMsg.getConnections(12)); } @Test @@ -130,20 +134,20 @@ public class RuleChainMsgConstructorTest { Assert.assertEquals("Connections count incorrect!", 10, ruleChainMetadataUpdateMsg.getConnectionsCount()); Assert.assertEquals("Rule chain connections count incorrect!", 1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount()); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(2, 5, "Success"), ruleChainMetadataUpdateMsg.getConnections(0)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 9, "Success"), ruleChainMetadataUpdateMsg.getConnections(1)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 9, "Success"), ruleChainMetadataUpdateMsg.getConnections(2)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 9, "Attributes Updated"), ruleChainMetadataUpdateMsg.getConnections(3)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 6, "RPC Request from Device"), ruleChainMetadataUpdateMsg.getConnections(4)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 3, "Post telemetry"), ruleChainMetadataUpdateMsg.getConnections(5)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 4, "Post attributes"), ruleChainMetadataUpdateMsg.getConnections(6)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 7, "Other"), ruleChainMetadataUpdateMsg.getConnections(7)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 8, "RPC Request to Device"), ruleChainMetadataUpdateMsg.getConnections(8)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, "Success"), ruleChainMetadataUpdateMsg.getConnections(9)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(2, 5, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(1)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(2)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 9, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(3)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 6, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(4)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 3, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(5)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 4, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(6)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 7, TbNodeConnectionType.OTHER), ruleChainMetadataUpdateMsg.getConnections(7)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 8, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(8)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(9)); RuleChainConnectionInfoProto ruleChainConnection = ruleChainMetadataUpdateMsg.getRuleChainConnections(0); Assert.assertEquals("From index incorrect!", 2, ruleChainConnection.getFromIndex()); - Assert.assertEquals("Type index incorrect!", "Success", ruleChainConnection.getType()); + Assert.assertEquals("Type index incorrect!", TbNodeConnectionType.SUCCESS, ruleChainConnection.getType()); Assert.assertEquals("Additional info incorrect!", "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}", ruleChainConnection.getAdditionalInfo()); @@ -172,20 +176,20 @@ public class RuleChainMsgConstructorTest { Assert.assertEquals("Connections count incorrect!", 10, ruleChainMetadataUpdateMsg.getConnectionsCount()); Assert.assertEquals("Rule chain connections count incorrect!", 1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount()); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, "Success"), ruleChainMetadataUpdateMsg.getConnections(0)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 0, "Attributes Updated"), ruleChainMetadataUpdateMsg.getConnections(1)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 3, "RPC Request from Device"), ruleChainMetadataUpdateMsg.getConnections(2)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 6, "Post telemetry"), ruleChainMetadataUpdateMsg.getConnections(3)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 5, "Post attributes"), ruleChainMetadataUpdateMsg.getConnections(4)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 2, "Other"), ruleChainMetadataUpdateMsg.getConnections(5)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 1, "RPC Request to Device"), ruleChainMetadataUpdateMsg.getConnections(6)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 0, "Success"), ruleChainMetadataUpdateMsg.getConnections(7)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 0, "Success"), ruleChainMetadataUpdateMsg.getConnections(8)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 4, "Success"), ruleChainMetadataUpdateMsg.getConnections(9)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 0, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(1)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 3, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(2)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 6, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(3)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 5, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(4)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 2, TbNodeConnectionType.OTHER), ruleChainMetadataUpdateMsg.getConnections(5)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 1, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(6)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(7)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(8)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 4, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(9)); RuleChainConnectionInfoProto ruleChainConnection = ruleChainMetadataUpdateMsg.getRuleChainConnections(0); Assert.assertEquals("From index incorrect!", 7, ruleChainConnection.getFromIndex()); - Assert.assertEquals("Type index incorrect!", "Success", ruleChainConnection.getType()); + Assert.assertEquals("Type index incorrect!", TbNodeConnectionType.SUCCESS, ruleChainConnection.getType()); Assert.assertEquals("Additional info incorrect!", "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}", ruleChainConnection.getAdditionalInfo()); @@ -225,19 +229,19 @@ public class RuleChainMsgConstructorTest { private List createConnections() { List result = new ArrayList<>(); - result.add(createNodeConnectionInfo(3, 6, "Success")); - result.add(createNodeConnectionInfo(3, 10, "Success")); - result.add(createNodeConnectionInfo(3, 0, "Success")); - result.add(createNodeConnectionInfo(4, 11, "Success")); - result.add(createNodeConnectionInfo(5, 11, "Success")); - result.add(createNodeConnectionInfo(6, 11, "Attributes Updated")); - result.add(createNodeConnectionInfo(6, 7, "RPC Request from Device")); - result.add(createNodeConnectionInfo(6, 4, "Post telemetry")); - result.add(createNodeConnectionInfo(6, 5, "Post attributes")); - result.add(createNodeConnectionInfo(6, 8, "Other")); - result.add(createNodeConnectionInfo(6, 9, "RPC Request to Device")); - result.add(createNodeConnectionInfo(7, 11, "Success")); - result.add(createNodeConnectionInfo(10, 9, "RPC")); + result.add(createNodeConnectionInfo(3, 6, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(3, 10, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(3, 0, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(4, 11, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(5, 11, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(6, 11, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(6, 7, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(6, 4, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(6, 5, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(6, 8, TbNodeConnectionType.OTHER)); + result.add(createNodeConnectionInfo(6, 9, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(7, 11, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(10, 9, RPC_CONNECTION_TYPE)); return result; } @@ -280,19 +284,19 @@ public class RuleChainMsgConstructorTest { private List createConnectionsInDifferentOrder() { List result = new ArrayList<>(); - result.add(createNodeConnectionInfo(0, 2, "RPC")); - result.add(createNodeConnectionInfo(4, 1, "Success")); - result.add(createNodeConnectionInfo(5, 1, "Attributes Updated")); - result.add(createNodeConnectionInfo(5, 4, "RPC Request from Device")); - result.add(createNodeConnectionInfo(5, 7, "Post telemetry")); - result.add(createNodeConnectionInfo(5, 6, "Post attributes")); - result.add(createNodeConnectionInfo(5, 3, "Other")); - result.add(createNodeConnectionInfo(5, 2, "RPC Request to Device")); - result.add(createNodeConnectionInfo(6, 1, "Success")); - result.add(createNodeConnectionInfo(7, 1, "Success")); - result.add(createNodeConnectionInfo(8, 11, "Success")); - result.add(createNodeConnectionInfo(8, 5, "Success")); - result.add(createNodeConnectionInfo(8, 0, "Success")); + result.add(createNodeConnectionInfo(0, 2, RPC_CONNECTION_TYPE)); + result.add(createNodeConnectionInfo(4, 1, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(5, 1, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(5, 4, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(5, 7, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(5, 6, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(5, 3, TbNodeConnectionType.OTHER)); + result.add(createNodeConnectionInfo(5, 2, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(6, 1, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(7, 1, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(8, 11, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(8, 5, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(8, 0, TbNodeConnectionType.SUCCESS)); return result; } diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java index 487783b454..b3e8941364 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.entitiy.alarm; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.junit.runner.RunWith; @@ -23,13 +24,20 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.edge.EdgeService; @@ -38,11 +46,13 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; +import java.util.Collections; +import java.util.List; import java.util.UUID; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -73,7 +83,6 @@ public class DefaultTbAlarmServiceTest { protected TbClusterService tbClusterService; @MockBean private EntitiesVersionControlService vcService; - @SpyBean DefaultTbAlarmService service; @@ -87,7 +96,7 @@ public class DefaultTbAlarmServiceTest { .build()); service.save(alarm, new User()); - verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); + verify(notificationEntityService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.ADDED), any()); verify(alarmSubscriptionService, times(1)).createAlarm(any()); } @@ -95,11 +104,11 @@ public class DefaultTbAlarmServiceTest { public void testAck() throws ThingsboardException { var alarm = new Alarm(); when(alarmSubscriptionService.acknowledgeAlarm(any(), any(), anyLong())) - .thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).build()); + .thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).alarm(new AlarmInfo()).build()); service.ack(alarm, new User(new UserId(UUID.randomUUID()))); verify(alarmCommentService, times(1)).saveAlarmComment(any(), any(), any()); - verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); + verify(notificationEntityService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.ALARM_ACK), any()); verify(alarmSubscriptionService, times(1)).acknowledgeAlarm(any(), any(), anyLong()); } @@ -108,11 +117,11 @@ public class DefaultTbAlarmServiceTest { var alarm = new Alarm(); alarm.setAcknowledged(true); when(alarmSubscriptionService.clearAlarm(any(), any(), anyLong(), any())) - .thenReturn(AlarmApiCallResult.builder().successful(true).cleared(true).build()); + .thenReturn(AlarmApiCallResult.builder().successful(true).cleared(true).alarm(new AlarmInfo()).build()); service.clear(alarm, new User(new UserId(UUID.randomUUID()))); verify(alarmCommentService, times(1)).saveAlarmComment(any(), any(), any()); - verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); + verify(notificationEntityService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.ALARM_CLEAR), any()); verify(alarmSubscriptionService, times(1)).clearAlarm(any(), any(), anyLong(), any()); } @@ -120,7 +129,64 @@ public class DefaultTbAlarmServiceTest { public void testDelete() { service.delete(new Alarm(), new User()); - verify(notificationEntityService, times(1)).notifyDeleteAlarm(any(), any(), any(), any(), any(), any(), anyString()); + verify(notificationEntityService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.DELETED), any()); verify(alarmSubscriptionService, times(1)).deleteAlarm(any(), any()); } -} \ No newline at end of file + + @Test + public void testUnassignAlarm() throws ThingsboardException { + AlarmInfo alarm = new AlarmInfo(); + alarm.setId(new AlarmId(UUID.randomUUID())); + when(alarmSubscriptionService.unassignAlarm(any(), any(), anyLong())) + .thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).alarm(alarm).build()); + + User user = new User(); + user.setEmail("testEmail@gmail.com"); + user.setId(new UserId(UUID.randomUUID())); + service.unassign(new Alarm(), 0L, user); + + ObjectNode commentNode = JacksonUtil.newObjectNode(); + commentNode.put("subtype", "ASSIGN"); + commentNode.put("text", "Alarm was unassigned by user " + user.getTitle()); + commentNode.put("userId", user.getId().getId().toString()); + AlarmComment expectedAlarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(commentNode) + .build(); + + verify(alarmCommentService, times(1)) + .saveAlarmComment(eq(alarm), eq(expectedAlarmComment), eq(user)); + } + + @Test + public void testUnassignDeletedUserAlarms() throws ThingsboardException { + AlarmInfo alarm = new AlarmInfo(); + alarm.setId(new AlarmId(UUID.randomUUID())); + + when(alarmService.findAlarmIdsByAssigneeId(any(), any(), any())) + .thenReturn(new PageData<>(List.of(alarm.getId()), 0, 1, false)) + .thenReturn(new PageData<>(Collections.EMPTY_LIST, 0, 0, false)); + when(alarmSubscriptionService.unassignAlarm(any(), any(), anyLong())) + .thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).alarm(alarm).build()); + + User user = new User(); + user.setEmail("testEmail@gmail.com"); + user.setId(new UserId(UUID.randomUUID())); + service.unassignDeletedUserAlarms(new TenantId(UUID.randomUUID()), user, System.currentTimeMillis()); + + ObjectNode commentNode = JacksonUtil.newObjectNode(); + commentNode.put("subtype", "ASSIGN"); + commentNode.put("text", String.format("Alarm was unassigned because user %s - was deleted", user.getTitle())); + AlarmComment expectedAlarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(commentNode) + .build(); + + verify(alarmCommentService, times(1)) + .saveAlarmComment(eq(alarm), eq(expectedAlarmComment), eq(null)); + } + + +} diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarmComment/DefaultTbAlarmCommentServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarmComment/DefaultTbAlarmCommentServiceTest.java index 29d9a6ebb8..50055b8d1a 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/alarmComment/DefaultTbAlarmCommentServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarmComment/DefaultTbAlarmCommentServiceTest.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentType; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.UserId; @@ -81,7 +82,7 @@ public class DefaultTbAlarmCommentServiceTest { when(alarmCommentService.createOrUpdateAlarmComment(Mockito.any(), eq(alarmComment))).thenReturn(alarmComment); service.saveAlarmComment(alarm, alarmComment, new User()); - verify(notificationEntityService, times(1)).notifyAlarmComment(any(), any(), any(), any()); + verify(notificationEntityService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.ADDED_COMMENT), any(), any()); } @Test @@ -95,7 +96,7 @@ public class DefaultTbAlarmCommentServiceTest { when(alarmCommentService.saveAlarmComment(Mockito.any(), eq(alarmComment))).thenReturn(alarmComment); service.deleteAlarmComment(new Alarm(alarmId), alarmComment, new User()); - verify(notificationEntityService, times(1)).notifyAlarmComment(any(), any(), any(), any()); + verify(notificationEntityService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.DELETED_COMMENT), any(), any()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/mail/TbMailSenderTest.java b/application/src/test/java/org/thingsboard/server/service/mail/TbMailSenderTest.java new file mode 100644 index 0000000000..5b95adf526 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/mail/TbMailSenderTest.java @@ -0,0 +1,95 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.mail; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.internet.MimeMessage; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willCallRealMethod; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; + +public class TbMailSenderTest { + + private TbMailSender tbMailSender; + + @BeforeEach + void setUp() { + tbMailSender = mock(TbMailSender.class); + } + + @Test + public void testDoSendSendMail() { + MimeMessage mimeMsg = new MimeMessage(Session.getInstance(new Properties())); + List mimeMessages = new ArrayList<>(1); + mimeMessages.add(mimeMsg); + + willCallRealMethod().given(tbMailSender).doSend(any(), any()); + tbMailSender.doSend(mimeMessages.toArray(new MimeMessage[0]), null); + + Mockito.verify(tbMailSender, times(1)).updateOauth2PasswordIfExpired(); + Mockito.verify(tbMailSender, times(1)).doSendSuper(any(), any()); + } + + @Test + public void testTestConnection() throws MessagingException { + willCallRealMethod().given(tbMailSender).testConnection(); + tbMailSender.testConnection(); + + Mockito.verify(tbMailSender, times(1)).updateOauth2PasswordIfExpired(); + Mockito.verify(tbMailSender, times(1)).testConnectionSuper(); + } + + @ParameterizedTest + @MethodSource("provideSenderConfiguration") + public void testUpdateOauth2PasswordIfExpiredIfOauth2Enabled(boolean oauth2, long expiresIn, boolean passwordUpdateNeeded) { + willReturn(oauth2).given(tbMailSender).getOauth2Enabled(); + willReturn(expiresIn).given(tbMailSender).getTokenExpires(); + + willCallRealMethod().given(tbMailSender).updateOauth2PasswordIfExpired(); + tbMailSender.updateOauth2PasswordIfExpired(); + + if (passwordUpdateNeeded) { + Mockito.verify(tbMailSender, times(1)).refreshAccessToken(); + Mockito.verify(tbMailSender, times(1)).setPassword(any()); + } else { + Mockito.verify(tbMailSender, Mockito.never()).refreshAccessToken(); + Mockito.verify(tbMailSender, Mockito.never()).setPassword(any()); + } + } + + private static Stream provideSenderConfiguration() { + return Stream.of( + Arguments.of(true, 0L, true), + Arguments.of(true, System.currentTimeMillis() + 5000, false), + Arguments.of(false, 0L, false), + Arguments.of(false, System.currentTimeMillis() + 5000, false) + ); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java index fb7675bd9e..104b5afef4 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java @@ -265,4 +265,8 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest return (NotificationApiWsClient) super.getWsClient(); } + @Override + public NotificationApiWsClient getAnotherWsClient() { + return (NotificationApiWsClient) super.getAnotherWsClient(); + } } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 851550eee7..1b3fce3c3c 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -15,15 +15,22 @@ */ package org.thingsboard.server.service.notification; +import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.assertj.core.data.Offset; import org.java_websocket.client.WebSocketClient; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.client.RestTemplate; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationRequest; @@ -33,32 +40,41 @@ import org.thingsboard.server.common.data.notification.NotificationRequestPrevie import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; +import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter; import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType; import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDeliveryMethodNotificationTemplate.Button.LinkType; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.notification.DefaultNotifications; import org.thingsboard.server.dao.notification.NotificationDao; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; @@ -67,6 +83,8 @@ import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; @DaoSqlTest @@ -79,6 +97,8 @@ public class NotificationApiTest extends AbstractNotificationApiTest { private NotificationDao notificationDao; @Autowired private DbCallbackExecutorService executor; + @Autowired + private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel; @Before public void beforeEach() throws Exception { @@ -470,6 +490,66 @@ public class NotificationApiTest extends AbstractNotificationApiTest { assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).hasValue(1); } + @Test + public void testUserNotificationSettings() throws Exception { + var entityActionNotificationPref = new UserNotificationSettings.NotificationPref(); + entityActionNotificationPref.setEnabled(true); + entityActionNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.WEB, true, + NotificationDeliveryMethod.SMS, false, + NotificationDeliveryMethod.EMAIL, false + )); + + var entitiesLimitNotificationPref = new UserNotificationSettings.NotificationPref(); + entitiesLimitNotificationPref.setEnabled(true); + entitiesLimitNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.SMS, true, + NotificationDeliveryMethod.WEB, false, + NotificationDeliveryMethod.EMAIL, false + )); + + var apiUsageLimitNotificationPref = new UserNotificationSettings.NotificationPref(); + apiUsageLimitNotificationPref.setEnabled(false); + apiUsageLimitNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.WEB, true, + NotificationDeliveryMethod.SMS, false, + NotificationDeliveryMethod.EMAIL, false + )); + + UserNotificationSettings settings = new UserNotificationSettings(Map.of( + NotificationType.ENTITY_ACTION, entityActionNotificationPref, + NotificationType.ENTITIES_LIMIT, entitiesLimitNotificationPref, + NotificationType.API_USAGE_LIMIT, apiUsageLimitNotificationPref + )); + doPost("/api/notification/settings/user", settings, UserNotificationSettings.class); + + var entityActionNotificationTemplate = createNotificationTemplate(NotificationType.ENTITY_ACTION, "Entity action", "Entity action", NotificationDeliveryMethod.WEB); + var entitiesLimitNotificationTemplate = createNotificationTemplate(NotificationType.ENTITIES_LIMIT, "Entities limit", "Entities limit", NotificationDeliveryMethod.WEB); + var apiUsageLimitNotificationTemplate = createNotificationTemplate(NotificationType.API_USAGE_LIMIT, "API usage limit", "API usage limit", NotificationDeliveryMethod.WEB); + NotificationTarget target = createNotificationTarget(tenantAdminUserId); + + NotificationRequest notificationRequest = NotificationRequest.builder() + .tenantId(tenantId) + .templateId(entityActionNotificationTemplate.getId()) + .originatorEntityId(tenantAdminUserId) + .targets(List.of(target.getUuidId())) + .ruleId(new NotificationRuleId(UUID.randomUUID())) // to trigger user settings check + .build(); + NotificationRequestStats stats = submitNotificationRequestAndWait(notificationRequest); + assertThat(stats.getErrors()).isEmpty(); + assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB).get()).isOne(); + + notificationRequest.setTemplateId(entitiesLimitNotificationTemplate.getId()); + stats = submitNotificationRequestAndWait(notificationRequest); + assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).matches(n -> n == null || n.get() == 0); + assertThat(stats.getErrors().get(NotificationDeliveryMethod.WEB).values()).first().asString().contains("disabled"); + + notificationRequest.setTemplateId(apiUsageLimitNotificationTemplate.getId()); + stats = submitNotificationRequestAndWait(notificationRequest); + assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).matches(n -> n == null || n.get() == 0); + assertThat(stats.getErrors().get(NotificationDeliveryMethod.WEB).values()).first().asString().contains("disabled"); + } + @Test public void testSlackNotifications() throws Exception { NotificationSettings settings = new NotificationSettings(); @@ -524,6 +604,94 @@ public class NotificationApiTest extends AbstractNotificationApiTest { assertThat(stats.getErrors().get(NotificationDeliveryMethod.SLACK).values()).containsExactly(errorMessage); } + @Test + public void testInternalGeneralWebNotifications() throws Exception { + loginSysAdmin(); + getAnotherWsClient().subscribeForUnreadNotifications(10).waitForReply(true); + + getAnotherWsClient().registerWaitForUpdate(); + + DefaultNotifications.DefaultNotification expectedNotification = DefaultNotifications.maintenanceWork; + notificationCenter.sendGeneralWebNotification(TenantId.SYS_TENANT_ID, new SystemAdministratorsFilter(), + expectedNotification.toTemplate()); + + getAnotherWsClient().waitForUpdate(true); + Notification notification = getAnotherWsClient().getLastDataUpdate().getUpdate(); + assertThat(notification.getSubject()).isEqualTo(expectedNotification.getSubject()); + assertThat(notification.getText()).isEqualTo(expectedNotification.getText()); + } + + @Test + public void testMicrosoftTeamsNotifications() throws Exception { + RestTemplate restTemplate = mock(RestTemplate.class); + microsoftTeamsNotificationChannel.setRestTemplate(restTemplate); + + String webhookUrl = "https://webhook.com/webhookb2/9628fa60-d873-11ed-913c-a196b1f9b445"; + var targetConfig = new MicrosoftTeamsNotificationTargetConfig(); + targetConfig.setWebhookUrl(webhookUrl); + targetConfig.setChannelName("My channel"); + NotificationTarget target = new NotificationTarget(); + target.setName("Microsoft Teams channel"); + target.setConfiguration(targetConfig); + target = saveNotificationTarget(target); + + var template = new MicrosoftTeamsDeliveryMethodNotificationTemplate(); + template.setEnabled(true); + String templateParams = "${recipientTitle} - ${entityType}"; + template.setSubject("Subject: " + templateParams); + template.setBody("Body: " + templateParams); + template.setThemeColor("ff0000"); + var button = new MicrosoftTeamsDeliveryMethodNotificationTemplate.Button(); + button.setEnabled(true); + button.setText("Button: " + templateParams); + button.setLinkType(LinkType.LINK); + button.setLink("https://" + templateParams); + template.setButton(button); + NotificationTemplate notificationTemplate = new NotificationTemplate(); + notificationTemplate.setName("Notification to Teams"); + notificationTemplate.setNotificationType(NotificationType.GENERAL); + NotificationTemplateConfig templateConfig = new NotificationTemplateConfig(); + templateConfig.setDeliveryMethodsTemplates(Map.of( + NotificationDeliveryMethod.MICROSOFT_TEAMS, template + )); + notificationTemplate.setConfiguration(templateConfig); + notificationTemplate = saveNotificationTemplate(notificationTemplate); + + NotificationRequest notificationRequest = NotificationRequest.builder() + .tenantId(tenantId) + .originatorEntityId(tenantAdminUserId) + .templateId(notificationTemplate.getId()) + .targets(List.of(target.getUuidId())) + .info(EntityActionNotificationInfo.builder() + .entityId(new DeviceId(UUID.randomUUID())) + .actionType(ActionType.ADDED) + .userId(tenantAdminUserId.getId()) + .build()) + .build(); + + NotificationRequestPreview preview = doPost("/api/notification/request/preview", notificationRequest, NotificationRequestPreview.class); + assertThat(preview.getRecipientsCountByTarget().get(target.getName())).isEqualTo(1); + assertThat(preview.getRecipientsPreview()).containsOnly(targetConfig.getChannelName()); + + var messageCaptor = ArgumentCaptor.forClass(MicrosoftTeamsNotificationChannel.Message.class); + notificationCenter.processNotificationRequest(tenantId, notificationRequest, null); + verify(restTemplate, timeout(20000)).postForEntity(eq(webhookUrl), messageCaptor.capture(), any()); + + var message = messageCaptor.getValue(); + String expectedParams = "My channel - Device"; + assertThat(message.getThemeColor()).isEqualTo(template.getThemeColor()); + assertThat(message.getSections().get(0).getActivityTitle()).isEqualTo("Subject: " + expectedParams); + assertThat(message.getSections().get(0).getActivitySubtitle()).isEqualTo("Body: " + expectedParams); + assertThat(message.getPotentialAction().get(0).getName()).isEqualTo("Button: " + expectedParams); + assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo("https://" + expectedParams); + } + + private NotificationRequestStats submitNotificationRequestAndWait(NotificationRequest notificationRequest) throws Exception { + SettableFuture future = SettableFuture.create(); + notificationCenter.processNotificationRequest(notificationRequest.getTenantId(), notificationRequest, future::set); + return future.get(30, TimeUnit.SECONDS); + } + private void checkFullNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String... expectedNotifications) { assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getText).containsOnly(expectedNotifications); assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getType).containsOnly(DEFAULT_NOTIFICATION_TYPE); @@ -540,7 +708,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { protected void connectOtherWsClient() throws Exception { loginCustomerUser(); - otherWsClient = (NotificationApiWsClient) super.getAnotherWsClient(); + otherWsClient = super.getAnotherWsClient(); loginTenantAdmin(); } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index f89730b689..0a2854a28d 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -342,7 +342,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { @Test public void testNotificationRuleProcessing_entitiesLimit() throws Exception { int limit = 5; - updateDefaultTenantProfile(profileConfiguration -> { + updateDefaultTenantProfileConfig(profileConfiguration -> { profileConfiguration.setMaxDevices(limit); profileConfiguration.setMaxAssets(limit); profileConfiguration.setMaxCustomers(limit); @@ -421,10 +421,10 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { int n = 10; updateDefaultTenantProfile(profileConfiguration -> { - profileConfiguration.setTenantEntityExportRateLimit(n + ":600"); - profileConfiguration.setCustomerServerRestLimitsConfiguration(n + ":600"); - profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(n + ":600"); - profileConfiguration.setTransportDeviceTelemetryMsgRateLimit(n + ":600"); + profileConfiguration.getProfileConfiguration().get().setTenantEntityExportRateLimit(n + ":600"); + profileConfiguration.getProfileConfiguration().get().setCustomerServerRestLimitsConfiguration(n + ":600"); + profileConfiguration.getProfileConfiguration().get().setTenantNotificationRequestsPerRuleRateLimit(n + ":600"); + profileConfiguration.getProfileConfiguration().get().setTransportDeviceTelemetryMsgRateLimit(n + ":600"); }); loginTenantAdmin(); NotificationRule rule = createNotificationRule(AlarmCommentNotificationRuleTriggerConfig.builder() @@ -608,7 +608,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { @Test public void testNotificationRequestsPerRuleRateLimits() throws Exception { int notificationRequestsLimit = 10; - updateDefaultTenantProfile(profileConfiguration -> { + updateDefaultTenantProfileConfig(profileConfiguration -> { profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(notificationRequestsLimit + ":300"); }); @@ -691,8 +691,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { int n = 5; updateDefaultTenantProfile(profileConfiguration -> { - profileConfiguration.setTenantEntityExportRateLimit(n + ":600"); - profileConfiguration.setTransportDeviceTelemetryMsgRateLimit(n + ":800"); + profileConfiguration.getProfileConfiguration().get().setTenantEntityExportRateLimit(n + ":600"); + profileConfiguration.getProfileConfiguration().get().setTransportDeviceTelemetryMsgRateLimit(n + ":800"); }); RateLimitsTrigger expectedTrigger = RateLimitsTrigger.builder() diff --git a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java b/application/src/test/java/org/thingsboard/server/service/notification/TestNotificationSettingsService.java similarity index 60% rename from application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java rename to application/src/test/java/org/thingsboard/server/service/notification/TestNotificationSettingsService.java index a49f8dc8cb..9b4ced8567 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/TestNotificationSettingsService.java @@ -19,14 +19,20 @@ import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.notification.DefaultNotificationSettingsService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.user.UserSettingsService; @Service @Primary -public class MockNotificationSettingsService extends DefaultNotificationSettingsService { +public class TestNotificationSettingsService extends DefaultNotificationSettingsService { - public MockNotificationSettingsService(AdminSettingsService adminSettingsService) { - super(adminSettingsService, null, null, null); + public TestNotificationSettingsService(AdminSettingsService adminSettingsService, + NotificationTargetService notificationTargetService, + NotificationTemplateService notificationTemplateService, + UserSettingsService userSettingsService) { + super(adminSettingsService, notificationTargetService, notificationTemplateService, null, userSettingsService); } @Override diff --git a/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackCallbackTest.java b/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackCallbackTest.java new file mode 100644 index 0000000000..80b9535af3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackCallbackTest.java @@ -0,0 +1,98 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.queue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.msg.queue.RuleEngineException; +import org.thingsboard.server.common.msg.queue.RuleNodeException; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; + +import java.util.UUID; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +class TbMsgPackCallbackTest { + + TenantId tenantId; + UUID msgId; + TbMsgPackProcessingContext ctx; + TbMsgPackCallback callback; + + @BeforeEach + void setUp() { + tenantId = TenantId.fromUUID(UUID.randomUUID()); + msgId = UUID.randomUUID(); + ctx = mock(TbMsgPackProcessingContext.class); + callback = spy(new TbMsgPackCallback(msgId, tenantId, ctx)); + } + + private static Stream testOnFailure_NotRateLimitException() { + return Stream.of( + Arguments.of(new RuleEngineException("rule engine no cause")), + Arguments.of(new RuleEngineException("rule engine caused 1 lvl", new RuntimeException())), + Arguments.of(new RuleEngineException("rule engine caused 2 lvl", new RuntimeException(new Exception()))), + Arguments.of(new RuleEngineException("rule engine caused 2 lvl Throwable", new RuntimeException(new Throwable()))), + Arguments.of(new RuleNodeException("rule node no cause", "RuleChain", new RuleNode())) + ); + } + + @ParameterizedTest + @MethodSource + void testOnFailure_NotRateLimitException(RuleEngineException ree) { + callback.onFailure(ree); + + verify(callback, never()).onRateLimit(any()); + verify(callback, never()).onSuccess(); + verify(ctx, never()).onSuccess(any()); + } + + private static Stream testOnFailure_RateLimitException() { + return Stream.of( + Arguments.of(new RuleEngineException("caused lvl 1", new TbRateLimitsException(EntityType.ASSET))), + Arguments.of(new RuleEngineException("caused lvl 2", new RuntimeException(new TbRateLimitsException(EntityType.ASSET)))), + Arguments.of( + new RuleEngineException("caused lvl 3", + new RuntimeException( + new Exception( + new TbRateLimitsException(EntityType.ASSET))))) + ); + } + + @ParameterizedTest + @MethodSource + void testOnFailure_RateLimitException(RuleEngineException ree) { + callback.onFailure(ree); + + verify(callback).onRateLimit(any()); + verify(callback).onFailure(any()); + verify(callback, never()).onSuccess(); + verify(ctx).onSuccess(msgId); + verify(ctx).onSuccess(any()); + verify(ctx, never()).onFailure(any(), any(), any()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java index 4373a4c108..5cd54a24eb 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java @@ -33,6 +33,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; @DaoSqlTest @TestPropertySource(properties = { @@ -121,7 +122,7 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest { } private String invokeScript(UUID scriptId, String msg) throws ExecutionException, InterruptedException { - return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get().toString(); + return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", POST_TELEMETRY_REQUEST.name()).get().toString(); } } diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java index ca5dbef5f1..62af9cb16e 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java @@ -42,6 +42,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; @DaoSqlTest @TestPropertySource(properties = { @@ -216,7 +217,7 @@ class TbelInvokeServiceTest extends AbstractControllerTest { private String invokeScript(UUID scriptId, String str) throws ExecutionException, InterruptedException { var msg = JacksonUtil.fromString(str, Map.class); - return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get().toString(); + return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", POST_TELEMETRY_REQUEST.name()).get().toString(); } } diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index b7dc40ec6d..7bb4f1e559 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -16,14 +16,22 @@ package org.thingsboard.server.service.security.auth; import io.jsonwebtoken.Claims; -import org.junit.BeforeClass; +import org.junit.Before; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.common.data.security.model.JwtToken; +import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService; +import org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsValidator; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -33,28 +41,40 @@ import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; import java.util.Calendar; import java.util.Date; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.willReturn; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class JwtTokenFactoryTest { - private static JwtTokenFactory tokenFactory; - private static JwtSettings jwtSettings; + private JwtTokenFactory tokenFactory; + private AdminSettingsService adminSettingsService; + private NotificationCenter notificationCenter; + private JwtSettingsService jwtSettingsService; - @BeforeClass - public static void beforeAll() { + private JwtSettings jwtSettings; + + @Before + public void beforeEach() { jwtSettings = new JwtSettings(); jwtSettings.setTokenIssuer("tb"); jwtSettings.setTokenSigningKey("abewafaf"); jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2)); jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7)); - JwtSettingsService jwtSettingsService = mock(JwtSettingsService.class); - willReturn(jwtSettings).given(jwtSettingsService).getJwtSettings(); + adminSettingsService = mock(AdminSettingsService.class); + notificationCenter = mock(NotificationCenter.class); + jwtSettingsService = mockJwtSettingsService(); + mockJwtSettings(jwtSettings); tokenFactory = new JwtTokenFactory(jwtSettingsService); } @@ -150,6 +170,33 @@ public class JwtTokenFactoryTest { }); } + @Test + public void testJwtSigningKeyIssueNotification() { + JwtSettings badJwtSettings = jwtSettings; + badJwtSettings.setTokenSigningKey(JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT); + mockJwtSettings(badJwtSettings); + jwtSettingsService = mockJwtSettingsService(); + + for (int i = 0; i < 5; i++) { // to check if notification is not sent twice + jwtSettingsService.getJwtSettings(); + } + verify(notificationCenter, times(1)).sendGeneralWebNotification(eq(TenantId.SYS_TENANT_ID), + isA(SystemAdministratorsFilter.class), argThat(template -> template.getConfiguration().getDeliveryMethodsTemplates().get(NotificationDeliveryMethod.WEB) + .getBody().contains("The platform is configured to use default JWT Signing Key"))); + } + + private void mockJwtSettings(JwtSettings settings) { + AdminSettings adminJwtSettings = new AdminSettings(); + adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(settings)); + when(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, JwtSettingsService.ADMIN_SETTINGS_JWT_KEY)) + .thenReturn(adminJwtSettings); + } + + private DefaultJwtSettingsService mockJwtSettingsService() { + return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), + Optional.of(notificationCenter), new DefaultJwtSettingsValidator()); + } + private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) { Claims claims = tokenFactory.parseTokenClaims(jwtToken).getBody(); assertThat(claims.getExpiration()).matches(actualExpirationTime -> { diff --git a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java index e70a888194..654f934ed2 100644 --- a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java @@ -33,11 +33,11 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -133,7 +133,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest void saveLatestTsForAssetAndDevice(List devices, Asset asset, int idx) throws ExecutionException, InterruptedException, TimeoutException { for (Device device : devices) { - TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, device.getId(), getTbMsgMetadata(device.getName(), ts.get(idx)), TbMsgDataType.JSON, diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 52f2ec5c9d..ccb0b5e26c 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -22,19 +22,31 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.sql.query.EntityQueryRepository; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; +import org.thingsboard.server.queue.usagestats.DefaultTbApiUsageReportClient; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -62,14 +74,32 @@ public class DefaultDeviceStateServiceTest { PartitionService partitionService; @Mock DeviceStateData deviceStateDataMock; + @Mock + EntityQueryRepository entityQueryRepository; + TenantId tenantId = new TenantId(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112")); DeviceId deviceId = DeviceId.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112"); + TopicPartitionInfo tpi; DefaultDeviceStateService service; + TelemetrySubscriptionService telemetrySubscriptionService; + @Before public void setUp() { - service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, null, null, null, mock(NotificationRuleProcessor.class))); + service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, entityQueryRepository, null, mock(DefaultTbApiUsageReportClient.class), mock(NotificationRuleProcessor.class))); + telemetrySubscriptionService = Mockito.mock(TelemetrySubscriptionService.class); + ReflectionTestUtils.setField(service, "tsSubService", telemetrySubscriptionService); + ReflectionTestUtils.setField(service, "defaultStateCheckIntervalInSec", 60); + ReflectionTestUtils.setField(service, "defaultActivityStatsIntervalInSec", 60); + ReflectionTestUtils.setField(service, "initFetchPackSize", 10); + + tpi = TopicPartitionInfo.builder().myPartition(true).build(); + Mockito.when(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(tpi); + Mockito.when(entityQueryRepository.findEntityDataByQueryInternal(Mockito.any())).thenReturn(new PageData<>()); + var deviceIdInfo = new DeviceIdInfo(tenantId.getId(), null, deviceId.getId()); + Mockito.when(deviceService.findDeviceIdInfos(Mockito.any())) + .thenReturn(new PageData<>(List.of(deviceIdInfo), 0, 1, false)); } @Test @@ -125,4 +155,190 @@ public class DefaultDeviceStateServiceTest { Assert.assertEquals(5000L, deviceStateData.getState().getInactivityTimeout()); } + private void initStateService(long timeout) throws InterruptedException { + service.stop(); + Mockito.reset(service, telemetrySubscriptionService); + ReflectionTestUtils.setField(service, "defaultInactivityTimeoutMs", timeout); + service.init(); + PartitionChangeEvent event = new PartitionChangeEvent(this, new QueueKey(ServiceType.TB_CORE), Collections.singleton(tpi)); + service.onApplicationEvent(event); + Thread.sleep(100); + } + + @Test + public void increaseInactivityForInactiveDeviceTest() throws Exception { + final long defaultTimeout = 1; + initStateService(defaultTimeout); + DeviceState deviceState = DeviceState.builder().build(); + DeviceStateData deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(deviceState) + .metaData(new TbMsgMetaData()) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + service.getPartitionedEntities(tpi).add(deviceId); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + Thread.sleep(defaultTimeout); + service.checkStates(); + activityVerify(false); + + Mockito.reset(telemetrySubscriptionService); + + long increase = 100; + long newTimeout = System.currentTimeMillis() - deviceState.getLastActivityTime() + increase; + + service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); + activityVerify(true); + Thread.sleep(increase); + service.checkStates(); + activityVerify(false); + + Mockito.reset(telemetrySubscriptionService); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + Thread.sleep(newTimeout + 5); + service.checkStates(); + activityVerify(false); + } + + @Test + public void increaseInactivityForActiveDeviceTest() throws Exception { + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + DeviceState deviceState = DeviceState.builder().build(); + DeviceStateData deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(deviceState) + .metaData(new TbMsgMetaData()) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + service.getPartitionedEntities(tpi).add(deviceId); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + + Mockito.reset(telemetrySubscriptionService); + + long increase = 100; + long newTimeout = System.currentTimeMillis() - deviceState.getLastActivityTime() + increase; + + service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); + Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any()); + Thread.sleep(defaultTimeout + increase); + service.checkStates(); + activityVerify(false); + + Mockito.reset(telemetrySubscriptionService); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + Thread.sleep(newTimeout); + service.checkStates(); + activityVerify(false); + } + + @Test + public void increaseSmallInactivityForInactiveDeviceTest() throws Exception { + final long defaultTimeout = 1; + initStateService(defaultTimeout); + DeviceState deviceState = DeviceState.builder().build(); + DeviceStateData deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(deviceState) + .metaData(new TbMsgMetaData()) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + service.getPartitionedEntities(tpi).add(deviceId); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + Thread.sleep(defaultTimeout); + service.checkStates(); + activityVerify(false); + + Mockito.reset(telemetrySubscriptionService); + + long newTimeout = 1; + Thread.sleep(newTimeout); + Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any()); + } + + @Test + public void decreaseInactivityForActiveDeviceTest() throws Exception { + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + DeviceState deviceState = DeviceState.builder().build(); + DeviceStateData deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(deviceState) + .metaData(new TbMsgMetaData()) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + service.getPartitionedEntities(tpi).add(deviceId); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + + Mockito.reset(telemetrySubscriptionService); + + Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any()); + + long newTimeout = 1; + Thread.sleep(newTimeout); + + service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); + activityVerify(false); + Mockito.reset(telemetrySubscriptionService); + + service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, defaultTimeout); + activityVerify(true); + Thread.sleep(defaultTimeout); + service.checkStates(); + activityVerify(false); + } + + @Test + public void decreaseInactivityForInactiveDeviceTest() throws Exception { + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + DeviceState deviceState = DeviceState.builder().build(); + DeviceStateData deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(deviceState) + .metaData(new TbMsgMetaData()) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + service.getPartitionedEntities(tpi).add(deviceId); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + Thread.sleep(defaultTimeout); + service.checkStates(); + activityVerify(false); + Mockito.reset(telemetrySubscriptionService); + + long newTimeout = 1; + Thread.sleep(newTimeout); + + service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); + Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any()); + } + + private void activityVerify(boolean isActive) { + Mockito.verify(telemetrySubscriptionService, Mockito.times(1)).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.eq(isActive), Mockito.any()); + } + } \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java index 39b61c6c47..b9b023e9e9 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java @@ -52,6 +52,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -343,7 +344,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2)); metaData.setFirstNodeIndex(0); - metaData.addConnectionInfo(0, 1, "Success"); + metaData.addConnectionInfo(0, 1, TbNodeConnectionType.SUCCESS); ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity()); return ruleChainService.findRuleChainById(tenantId, ruleChain.getId()); @@ -381,7 +382,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2)); metaData.setFirstNodeIndex(0); - metaData.addConnectionInfo(0, 1, "Success"); + metaData.addConnectionInfo(0, 1, TbNodeConnectionType.SUCCESS); ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity()); return ruleChainService.findRuleChainById(tenantId, ruleChain.getId()); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index e493916b8a..3361d516f0 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -53,6 +53,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.sync.ie.DeviceExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData; @@ -65,9 +66,12 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.ota.OtaPackageStateService; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -338,6 +342,47 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { checkImportedRuleChainData(ruleChain, metaData, importedRuleChain, importedMetaData); } + @Test + public void testImportRuleChain_ruleNodesConfigs() throws Exception { + Customer customer = createCustomer(tenantId1, "Customer 1"); + RuleChain ruleChain = createRuleChain(tenantId1, "Rule chain 1"); + RuleChainMetaData metaData = ruleChainService.loadRuleChainMetaData(tenantId1, ruleChain.getId()); + + List nodes = new ArrayList<>(metaData.getNodes()); + RuleNode generatorNode = new RuleNode(); + generatorNode.setName("Generator"); + generatorNode.setType(TbMsgGeneratorNode.class.getName()); + TbMsgGeneratorNodeConfiguration generatorNodeConfig = new TbMsgGeneratorNodeConfiguration(); + generatorNodeConfig.setOriginatorType(EntityType.ASSET_PROFILE); + generatorNodeConfig.setOriginatorId(customer.getId().toString()); + generatorNodeConfig.setPeriodInSeconds(5); + generatorNodeConfig.setMsgCount(1); + generatorNodeConfig.setScriptLang(ScriptLanguage.JS); + UUID someUuid = UUID.randomUUID(); + generatorNodeConfig.setJsScript("var msg = { temp: 42, humidity: 77 };\n" + + "var metadata = { data: 40 };\n" + + "var msgType = \"POST_TELEMETRY_REQUEST\";\n" + + "var someUuid = \"" + someUuid + "\";\n" + + "return { msg: msg, metadata: metadata, msgType: msgType };"); + generatorNode.setConfiguration(JacksonUtil.valueToTree(generatorNodeConfig)); + nodes.add(generatorNode); + metaData.setNodes(nodes); + ruleChainService.saveRuleChainMetaData(tenantId1, metaData, Function.identity()); + + EntityExportData ruleChainExportData = exportEntity(tenantAdmin1, ruleChain.getId()); + EntityExportData customerExportData = exportEntity(tenantAdmin1, customer.getId()); + + Customer importedCustomer = importEntity(tenantAdmin2, customerExportData).getSavedEntity(); + RuleChain importedRuleChain = importEntity(tenantAdmin2, ruleChainExportData).getSavedEntity(); + RuleChainMetaData importedMetaData = ruleChainService.loadRuleChainMetaData(tenantId2, importedRuleChain.getId()); + + TbMsgGeneratorNodeConfiguration importedGeneratorNodeConfig = JacksonUtil.treeToValue(importedMetaData.getNodes().stream() + .filter(node -> node.getName().equals(generatorNode.getName())) + .findFirst().get().getConfiguration(), TbMsgGeneratorNodeConfiguration.class); + assertThat(importedGeneratorNodeConfig.getOriginatorId()).isEqualTo(importedCustomer.getId().toString()); + assertThat(importedGeneratorNodeConfig.getJsScript()).contains("var someUuid = \"" + someUuid + "\";"); + } + @Test public void testExportImportWithInboundRelations_betweenTenants() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/service/ttl/AlarmsCleanUpServiceTest.java b/application/src/test/java/org/thingsboard/server/service/ttl/AlarmsCleanUpServiceTest.java index 6cabff788e..b2c06b419b 100644 --- a/application/src/test/java/org/thingsboard/server/service/ttl/AlarmsCleanUpServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/ttl/AlarmsCleanUpServiceTest.java @@ -67,7 +67,7 @@ public class AlarmsCleanUpServiceTest extends AbstractControllerTest { @Test public void testAlarmsCleanUp() throws Exception { int ttlDays = 1; - updateDefaultTenantProfile(profileConfiguration -> { + updateDefaultTenantProfileConfig(profileConfiguration -> { profileConfiguration.setAlarmsTtlDays(ttlDays); }); @@ -104,10 +104,10 @@ public class AlarmsCleanUpServiceTest extends AbstractControllerTest { alarmsCleanUpService.cleanUp(); for (AlarmId outdatedAlarm : outdatedAlarms) { - verify(alarmService).delAlarm(eq(tenantId), eq(outdatedAlarm)); + verify(alarmService).delAlarm(eq(tenantId), eq(outdatedAlarm), eq(false)); } for (AlarmId freshAlarm : freshAlarms) { - verify(alarmService, never()).delAlarm(eq(tenantId), eq(freshAlarm)); + verify(alarmService, never()).delAlarm(eq(tenantId), eq(freshAlarm), eq(false)); } verify(cleanUpServiceLogger).info(startsWith("Removed {} outdated alarm"), eq((long) count), eq(tenantId), any()); diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index b72c0c9800..a964b0b700 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -68,3 +68,5 @@ sql.ttl.audit_logs.ttl=2592000 sql.edge_events.partition_size=168 sql.ttl.edge_events.edge_event_ttl=2592000 + +server.log_controller_error_stack_trace=false diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 14db2eea7b..981bcab132 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -28,6 +28,10 @@ + + + + diff --git a/common/actor/pom.xml b/common/actor/pom.xml index b113788a11..2de85be932 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorCtx.java b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorCtx.java index 2a8f641c4e..bbe19fb3dd 100644 --- a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorCtx.java +++ b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorCtx.java @@ -32,7 +32,7 @@ public interface TbActorCtx extends TbActorRef { void stop(TbActorId target); - TbActorRef getOrCreateChildActor(TbActorId actorId, Supplier dispatcher, Supplier creator); + TbActorRef getOrCreateChildActor(TbActorId actorId, Supplier dispatcher, Supplier creator, Supplier createCondition); void broadcastToChildren(TbActorMsg msg); diff --git a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java index ad1604f7b0..34537c143c 100644 --- a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java +++ b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java @@ -15,7 +15,8 @@ */ package org.thingsboard.server.actors; -import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.msg.MsgType; @@ -31,7 +32,8 @@ import java.util.function.Predicate; import java.util.function.Supplier; @Slf4j -@Data +@Getter +@RequiredArgsConstructor public final class TbActorMailbox implements TbActorCtx { private static final boolean HIGH_PRIORITY = true; private static final boolean NORMAL_PRIORITY = false; @@ -212,9 +214,9 @@ public final class TbActorMailbox implements TbActorCtx { } @Override - public TbActorRef getOrCreateChildActor(TbActorId actorId, Supplier dispatcher, Supplier creator) { + public TbActorRef getOrCreateChildActor(TbActorId actorId, Supplier dispatcher, Supplier creator, Supplier createCondition) { TbActorRef actorRef = system.getActor(actorId); - if (actorRef == null) { + if (actorRef == null && createCondition.get()) { return system.createChildActor(dispatcher.get(), creator.get(), selfId); } else { return actorRef; diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 0a9141846f..440860fda6 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index d6954a40f9..c9fa6d0894 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 8962a35187..e81aaed39d 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -80,8 +80,6 @@ public interface TbClusterService extends TbQueueClusterService { void onDeviceUpdated(Device device, Device old); - void onDeviceUpdated(Device device, Device old, boolean notifyEdge); - void onDeviceDeleted(Device device, TbQueueCallback callback); void onResourceChange(TbResource resource, TbQueueCallback callback); diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java index 4b2bde733e..19aa0284ea 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java @@ -17,7 +17,11 @@ package org.thingsboard.server.queue; public interface TbQueueAdmin { - void createTopicIfNotExists(String topic); + default void createTopicIfNotExists(String topic) { + createTopicIfNotExists(topic, null); + } + + void createTopicIfNotExists(String topic, String properties); void destroy(); diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java index 04439fc85d..9c41f9d342 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java @@ -36,4 +36,10 @@ public interface TbQueueConsumer { boolean isStopped(); + void onQueueDelete(); + + boolean isQueueDeleted(); + + List getFullTopicNames(); + } diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 80f44e59be..78614911d4 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -28,6 +28,7 @@ message ServiceInfo { repeated string serviceTypes = 2; repeated string transports = 6; SystemInfoProto systemInfo = 10; + repeated string assignedTenantProfiles = 11; } message SystemInfoProto { diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 116a94791d..420fc4dae6 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index cf825ea637..029c660500 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index fbf8892bd5..5c8b88e4b8 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.alarm; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; @@ -33,12 +34,14 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.Collection; +import java.util.Set; public interface AlarmService extends EntityDaoService { @@ -74,6 +77,10 @@ public interface AlarmService extends EntityDaoService { AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId); + AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId, boolean checkAndDeleteAlarmType); + + void delAlarmTypes(TenantId tenantId, Set types); + /* * Legacy API, before 3.5. */ @@ -118,7 +125,11 @@ public interface AlarmService extends EntityDaoService { PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); + PageData findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, PageLink pageLink); + void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId); long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); + + PageData findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java index f89153feee..1ec6fb3fb5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java @@ -31,6 +31,8 @@ public interface AssetProfileService extends EntityDaoService { AssetProfileInfo findAssetProfileInfoById(TenantId tenantId, AssetProfileId assetProfileId); + AssetProfile saveAssetProfile(AssetProfile assetProfile, boolean doValidate); + AssetProfile saveAssetProfile(AssetProfile assetProfile); void deleteAssetProfile(TenantId tenantId, AssetProfileId assetProfileId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java index 8b825e0837..722eeff365 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java @@ -41,6 +41,8 @@ public interface AssetService extends EntityDaoService { Asset findAssetByTenantIdAndName(TenantId tenantId, String name); + Asset saveAsset(Asset asset, boolean doValidate); + Asset saveAsset(Asset asset); Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, CustomerId customerId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index c9974bba69..434088180c 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -40,6 +40,8 @@ public interface DashboardService extends EntityDaoService { ListenableFuture findDashboardInfoByIdAsync(TenantId tenantId, DashboardId dashboardId); + Dashboard saveDashboard(Dashboard dashboard, boolean doValidate); + Dashboard saveDashboard(Dashboard dashboard); Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, CustomerId customerId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java new file mode 100644 index 0000000000..90355d885d --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.core.io.Resource; +import org.thingsboard.server.common.data.Device; + +import java.net.URISyntaxException; + +public interface DeviceConnectivityService { + + JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException; + + Resource getPemCertFile(String protocol); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java index e765cc5030..f94b709c97 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java @@ -31,6 +31,8 @@ public interface DeviceProfileService extends EntityDaoService { DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId); + DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile, boolean doValidate); + DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile); void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java index 064aa39a80..064782df06 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.EntityDaoService; @@ -83,6 +84,8 @@ public interface EdgeService extends EntityDaoService { PageData findEdgesByTenantIdAndEntityId(TenantId tenantId, EntityId ruleChainId, PageLink pageLink); + PageData findEdgesByTenantProfileId(TenantProfileId tenantProfileId, PageLink pageLink); + List findAllRelatedEdgeIds(TenantId tenantId, EntityId entityId); PageData findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbRelationTypes.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeSynchronizationManager.java similarity index 74% rename from rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbRelationTypes.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeSynchronizationManager.java index 2535762ad5..8cce581098 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbRelationTypes.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeSynchronizationManager.java @@ -13,14 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.api; +package org.thingsboard.server.dao.edge; -/** - * Created by ashvayka on 19.01.18. - */ -public final class TbRelationTypes { +public interface EdgeSynchronizationManager { - public static String SUCCESS = "Success"; - public static String FAILURE = "Failure"; + ThreadLocal getSync(); + boolean isSync(); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java index 31b2004a38..ea9dc31b45 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java @@ -38,6 +38,8 @@ public interface EntityViewService extends EntityDaoService { EntityView saveEntityView(EntityView entityView); + EntityView saveEntityView(EntityView entityView, boolean doValidate); + EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId); EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/housekeeper/HouseKeeperService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/housekeeper/HouseKeeperService.java new file mode 100644 index 0000000000..62f53e0f41 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/housekeeper/HouseKeeperService.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.housekeeper; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.List; + +public interface HouseKeeperService { + + ListenableFuture> unassignDeletedUserAlarms(TenantId tenantId, User user, long unassignTs); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java index a5433915b3..9fca174b3a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java @@ -16,7 +16,9 @@ package org.thingsboard.server.dao.notification; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; public interface NotificationSettingsService { @@ -24,6 +26,10 @@ public interface NotificationSettingsService { NotificationSettings findNotificationSettings(TenantId tenantId); + UserNotificationSettings saveUserNotificationSettings(TenantId tenantId, UserId userId, UserNotificationSettings settings); + + UserNotificationSettings getUserNotificationSettings(TenantId tenantId, UserId userId, boolean format); + void createDefaultNotificationConfigs(TenantId tenantId); void updateDefaultNotificationConfigs(TenantId tenantId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java index 3ddb6187ea..39e94b3a6b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -53,6 +53,8 @@ public interface RelationService { void deleteEntityRelations(TenantId tenantId, EntityId entity); + void deleteEntityCommonRelations(TenantId tenantId, EntityId entity); + List findByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); ListenableFuture> findByFromAsync(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index e1f16be1ed..b874e038d2 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -30,34 +30,34 @@ import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.List; public interface UserService extends EntityDaoService { - - User findUserById(TenantId tenantId, UserId userId); - ListenableFuture findUserByIdAsync(TenantId tenantId, UserId userId); + User findUserById(TenantId tenantId, UserId userId); - User findUserByEmail(TenantId tenantId, String email); + ListenableFuture findUserByIdAsync(TenantId tenantId, UserId userId); + + User findUserByEmail(TenantId tenantId, String email); User findUserByTenantIdAndEmail(TenantId tenantId, String email); - User saveUser(User user); + User saveUser(TenantId tenantId, User user); + + UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId); + + UserCredentials findUserCredentialsByActivateToken(TenantId tenantId, String activateToken); + + UserCredentials findUserCredentialsByResetToken(TenantId tenantId, String resetToken); - UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId); - - UserCredentials findUserCredentialsByActivateToken(TenantId tenantId, String activateToken); + UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials); - UserCredentials findUserCredentialsByResetToken(TenantId tenantId, String resetToken); + UserCredentials activateUserCredentials(TenantId tenantId, String activateToken, String password); - UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials); - - UserCredentials activateUserCredentials(TenantId tenantId, String activateToken, String password); - - UserCredentials requestPasswordReset(TenantId tenantId, String email); + UserCredentials requestPasswordReset(TenantId tenantId, String email); UserCredentials requestExpiredPasswordReset(TenantId tenantId, UserCredentialsId userCredentialsId); UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); - void deleteUser(TenantId tenantId, UserId userId); + void deleteUser(TenantId tenantId, User user); PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink); 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 245e33ed14..edc6bb21da 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 @@ -15,9 +15,11 @@ */ package org.thingsboard.server.dao.widget; -import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; +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.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; @@ -31,20 +33,32 @@ public interface WidgetTypeService extends EntityDaoService { WidgetTypeDetails findWidgetTypeDetailsById(TenantId tenantId, WidgetTypeId widgetTypeId); + boolean widgetTypeExistsByTenantIdAndWidgetTypeId(TenantId tenantId, WidgetTypeId widgetTypeId); + WidgetTypeDetails saveWidgetType(WidgetTypeDetails widgetType); void deleteWidgetType(TenantId tenantId, WidgetTypeId widgetTypeId); - List findWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias); + PageData findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink); + + PageData findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink); + + PageData findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink); + + List findWidgetTypesByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId); + + List findWidgetTypesDetailsByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId); + + List findWidgetTypesInfosByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId); - List findWidgetTypesDetailsByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias); + List findWidgetFqnsByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId); - List findWidgetTypesInfosByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias); + WidgetType findWidgetTypeByTenantIdAndFqn(TenantId tenantId, String fqn); - List findWidgetTypesInfosByTenantIdAndResourceId(TenantId tenantId, TbResourceId tbResourceId); + void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List widgetTypeIds); - WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias); + void updateWidgetsBundleWidgetFqns(TenantId tenantId, WidgetsBundleId widgetsBundleId, List widgetFqns); - void deleteWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias); + void deleteWidgetTypesByTenantId(TenantId tenantId); } diff --git a/common/data/pom.xml b/common/data/pom.xml index 35e9d1dc55..d3651c332a 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index f21b13a674..e06409d177 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -45,4 +45,5 @@ public class CacheConstants { public static final String DASHBOARD_TITLES_CACHE = "dashboardTitles"; public static final String ENTITY_COUNT_CACHE = "entityCount"; public static final String RESOURCE_INFO_CACHE = "resourceInfo"; + public static final String ALARM_TYPES_CACHE = "alarmTypes"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 0f0cf0467f..31c768bd76 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -109,6 +109,7 @@ public class DataConstants { public static final String PROVISION_KEY = "provisionDeviceKey"; public static final String PROVISION_SECRET = "provisionDeviceSecret"; + public static final String DEFAULT_DEVICE_TYPE = "default"; public static final String DEVICE_NAME = "deviceName"; public static final String DEVICE_TYPE = "deviceType"; public static final String CERT_PUB_KEY = "x509CertPubKey"; @@ -123,6 +124,8 @@ public class DataConstants { public static final String LAST_CONNECTED_GATEWAY = "lastConnectedGateway"; + public static final String MQTT_TOPIC = "mqttTopic"; + public static final String MAIN_QUEUE_NAME = "Main"; public static final String MAIN_QUEUE_TOPIC = "tb_rule_engine.main"; public static final String HP_QUEUE_NAME = "HighPriority"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java index b1c5f39611..f74ea6fd07 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java @@ -25,60 +25,35 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import java.util.EnumMap; import java.util.concurrent.ThreadLocalRandom; @Slf4j public final class EdgeUtils { - private static final int STACK_TRACE_LIMIT = 10; - - private EdgeUtils() { - } + private static final EnumMap entityTypeEdgeEventTypeEnumMap; - public static EdgeEventType getEdgeEventTypeByEntityType(EntityType entityType) { - switch (entityType) { - case EDGE: - return EdgeEventType.EDGE; - case DEVICE: - return EdgeEventType.DEVICE; - case DEVICE_PROFILE: - return EdgeEventType.DEVICE_PROFILE; - case ASSET: - return EdgeEventType.ASSET; - case ASSET_PROFILE: - return EdgeEventType.ASSET_PROFILE; - case ENTITY_VIEW: - return EdgeEventType.ENTITY_VIEW; - case DASHBOARD: - return EdgeEventType.DASHBOARD; - case USER: - return EdgeEventType.USER; - case RULE_CHAIN: - return EdgeEventType.RULE_CHAIN; - case ALARM: - return EdgeEventType.ALARM; - case TENANT: - return EdgeEventType.TENANT; - case CUSTOMER: - return EdgeEventType.CUSTOMER; - case WIDGETS_BUNDLE: - return EdgeEventType.WIDGETS_BUNDLE; - case WIDGET_TYPE: - return EdgeEventType.WIDGET_TYPE; - case OTA_PACKAGE: - return EdgeEventType.OTA_PACKAGE; - case QUEUE: - return EdgeEventType.QUEUE; - default: - log.warn("Unsupported entity type [{}]", entityType); - return null; + static { + entityTypeEdgeEventTypeEnumMap = new EnumMap<>(EntityType.class); + for (EdgeEventType edgeEventType : EdgeEventType.values()) { + if (edgeEventType.getEntityType() != null) { + entityTypeEdgeEventTypeEnumMap.put(edgeEventType.getEntityType(), edgeEventType); + } } } + private static final int STACK_TRACE_LIMIT = 10; + + private EdgeUtils() {} + public static int nextPositiveInt() { return ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE); } + public static EdgeEventType getEdgeEventTypeByEntityType(EntityType entityType) { + return entityTypeEdgeEventTypeEnumMap.get(entityType); + } + public static EdgeEvent constructEdgeEvent(TenantId tenantId, EdgeId edgeId, EdgeEventType type, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityFieldsData.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityFieldsData.java index c88a6b970f..db069d8d61 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityFieldsData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityFieldsData.java @@ -68,20 +68,22 @@ public class EntityFieldsData { break; } } - if (current != null) { - if(current.isNull() && ignoreNullStrings){ + if (current == null) { + return null; + } + if (current.isNull() && ignoreNullStrings) { + return null; + } + if (current.isValueNode()) { + String textValue = current.asText(); + if (StringUtils.isEmpty(textValue) && ignoreNullStrings) { return null; } - if (current.isValueNode()) { - return current.asText(); - } else { - try { - return mapper.writeValueAsString(current); - } catch (JsonProcessingException e) { - return null; - } - } - } else { + return textValue; + } + try { + return mapper.writeValueAsString(current); + } catch (JsonProcessingException e) { return null; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 572dc16c0b..8ca6585718 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -18,6 +18,10 @@ package org.thingsboard.server.common.data; import lombok.Getter; import org.apache.commons.lang3.StringUtils; +import java.util.EnumSet; +import java.util.List; +import java.util.stream.Collectors; + /** * @author Andrew Shvayka */ @@ -31,7 +35,13 @@ public enum EntityType { ALARM, RULE_CHAIN, RULE_NODE, - ENTITY_VIEW, + ENTITY_VIEW { + // backward compatibility for TbOriginatorTypeSwitchNode to return correct rule node connection. + @Override + public String getNormalName() { + return "Entity View"; + } + }, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, @@ -49,6 +59,9 @@ public enum EntityType { NOTIFICATION, NOTIFICATION_RULE; + public static final List NORMAL_NAMES = EnumSet.allOf(EntityType.class).stream() + .map(EntityType::getNormalName).collect(Collectors.toUnmodifiableList()); + @Getter private final String normalName = StringUtils.capitalize(StringUtils.removeStart(name(), "TB_") .toLowerCase().replaceAll("_", " ")); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java index a7671f4327..7360f9b8ab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -181,6 +181,13 @@ public class StringUtils { return org.apache.commons.lang3.StringUtils.contains(seq, searchSeq); } + /** + * Use this to prevent org.postgresql.util.PSQLException: ERROR: invalid byte sequence for encoding "UTF8": 0x00 + **/ + public static boolean contains0x00(final String s) { + return s != null && s.contains("\u0000"); + } + public static String randomNumeric(int length) { return RandomStringUtils.randomNumeric(length); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java index 20ed7b549c..43680242e5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.alarm; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -22,6 +23,8 @@ import org.thingsboard.server.common.data.id.UserId; import java.io.Serializable; +import static org.apache.commons.lang3.StringUtils.isNotEmpty; + @Builder @AllArgsConstructor @Data @@ -34,4 +37,22 @@ public class AlarmAssignee implements Serializable { private final String lastName; private final String email; + @JsonIgnore + public String getTitle() { + String title = ""; + if (isNotEmpty(firstName)) { + title += firstName; + } + if (isNotEmpty(lastName)) { + if (!title.isEmpty()) { + title += " "; + } + title += lastName; + } + if (title.isEmpty()) { + title = email; + } + return title; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java index 982e0f94d1..76ea06d27a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java @@ -22,6 +22,7 @@ import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.id.AlarmCommentId; @@ -44,6 +45,7 @@ public class AlarmComment extends BaseData implements HasName { @ApiModelProperty(position = 6, value = "JSON object with text of comment.", dataType = "com.fasterxml.jackson.databind.JsonNode") @NoXss @Length(fieldName = "comment", max = 10000) + @EqualsAndHashCode.Include private transient JsonNode comment; @ApiModelProperty(position = 1, value = "JSON object with the alarm comment Id. " + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 01009751d4..056be5a958 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -16,49 +16,61 @@ package org.thingsboard.server.common.data.audit; import lombok.Getter; +import org.thingsboard.server.common.data.msg.TbMsgType; + +import java.util.Optional; -@Getter public enum ActionType { - ADDED(false), // log entity - DELETED(false), // log string id - UPDATED(false), // log entity - ATTRIBUTES_UPDATED(false), // log attributes/values - ATTRIBUTES_DELETED(false), // log attributes - TIMESERIES_UPDATED(false), // log timeseries update - TIMESERIES_DELETED(false), // log timeseries - RPC_CALL(false), // log method and params - CREDENTIALS_UPDATED(false), // log new credentials - ASSIGNED_TO_CUSTOMER(false), // log customer name - UNASSIGNED_FROM_CUSTOMER(false), // log customer name - ACTIVATED(false), // log string id - SUSPENDED(false), // log string id - CREDENTIALS_READ(true), // log device id - ATTRIBUTES_READ(true), // log attributes - RELATION_ADD_OR_UPDATE(false), - RELATION_DELETED(false), - RELATIONS_DELETED(false), - ALARM_ACK(false), - ALARM_CLEAR(false), - ALARM_DELETE(false), - ALARM_ASSIGNED(false), - ALARM_UNASSIGNED(false), - LOGIN(false), - LOGOUT(false), - LOCKOUT(false), - ASSIGNED_FROM_TENANT(false), - ASSIGNED_TO_TENANT(false), - PROVISION_SUCCESS(false), - PROVISION_FAILURE(false), - ASSIGNED_TO_EDGE(false), // log edge name - UNASSIGNED_FROM_EDGE(false), - ADDED_COMMENT(false), - UPDATED_COMMENT(false), - DELETED_COMMENT(false), - SMS_SENT(false); + ADDED(false, TbMsgType.ENTITY_CREATED), // log entity + DELETED(false, TbMsgType.ENTITY_DELETED), // log string id + UPDATED(false, TbMsgType.ENTITY_UPDATED), // log entity + ATTRIBUTES_UPDATED(false, TbMsgType.ATTRIBUTES_UPDATED), // log attributes/values + ATTRIBUTES_DELETED(false, TbMsgType.ATTRIBUTES_DELETED), // log attributes + TIMESERIES_UPDATED(false, TbMsgType.TIMESERIES_UPDATED), // log timeseries update + TIMESERIES_DELETED(false, TbMsgType.TIMESERIES_DELETED), // log timeseries + RPC_CALL(false, null), // log method and params + CREDENTIALS_UPDATED(false, null), // log new credentials + ASSIGNED_TO_CUSTOMER(false, TbMsgType.ENTITY_ASSIGNED), // log customer name + UNASSIGNED_FROM_CUSTOMER(false, TbMsgType.ENTITY_UNASSIGNED), // log customer name + ACTIVATED(false, null), // log string id + SUSPENDED(false, null), // log string id + CREDENTIALS_READ(true, null), // log device id + ATTRIBUTES_READ(true, null), // log attributes + RELATION_ADD_OR_UPDATE(false, TbMsgType.RELATION_ADD_OR_UPDATE), + RELATION_DELETED(false, TbMsgType.RELATION_DELETED), + RELATIONS_DELETED(false, TbMsgType.RELATIONS_DELETED), + ALARM_ACK(false, TbMsgType.ALARM_ACK), + ALARM_CLEAR(false, TbMsgType.ALARM_CLEAR), + ALARM_DELETE(false, TbMsgType.ALARM_DELETE), + ALARM_ASSIGNED(false, TbMsgType.ALARM_ASSIGNED), + ALARM_UNASSIGNED(false, TbMsgType.ALARM_UNASSIGNED), + LOGIN(false, null), + LOGOUT(false, null), + LOCKOUT(false, null), + ASSIGNED_FROM_TENANT(false, TbMsgType.ENTITY_ASSIGNED_FROM_TENANT), + ASSIGNED_TO_TENANT(false, TbMsgType.ENTITY_ASSIGNED_TO_TENANT), + PROVISION_SUCCESS(false, TbMsgType.PROVISION_SUCCESS), + PROVISION_FAILURE(false, TbMsgType.PROVISION_FAILURE), + ASSIGNED_TO_EDGE(false, TbMsgType.ENTITY_ASSIGNED_TO_EDGE), // log edge name + UNASSIGNED_FROM_EDGE(false, TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE), + ADDED_COMMENT(false, TbMsgType.COMMENT_CREATED), + UPDATED_COMMENT(false, TbMsgType.COMMENT_UPDATED), + DELETED_COMMENT(false, null), + SMS_SENT(false, null); + + @Getter private final boolean isRead; - ActionType(boolean isRead) { + private final TbMsgType ruleEngineMsgType; + + ActionType(boolean isRead, TbMsgType ruleEngineMsgType) { this.isRead = isRead; + this.ruleEngineMsgType = ruleEngineMsgType; } + + public Optional getRuleEngineMsgType() { + return Optional.ofNullable(ruleEngineMsgType); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java index 389b87f813..e51a0b5dea 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java @@ -27,6 +27,8 @@ import java.util.List; @Data public class DeviceProfileData implements Serializable { + private static final long serialVersionUID = -3864805547939495272L; + @ApiModelProperty(position = 1, value = "JSON object of device profile configuration") private DeviceProfileConfiguration configuration; @Valid diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 756c30690f..1397deb5d7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -15,24 +15,39 @@ */ package org.thingsboard.server.common.data.edge; +import lombok.Getter; +import org.thingsboard.server.common.data.EntityType; + +@Getter public enum EdgeEventType { - DASHBOARD, - ASSET, - DEVICE, - DEVICE_PROFILE, - ASSET_PROFILE, - ENTITY_VIEW, - ALARM, - RULE_CHAIN, - RULE_CHAIN_METADATA, - EDGE, - USER, - CUSTOMER, - RELATION, - TENANT, - WIDGETS_BUNDLE, - WIDGET_TYPE, - ADMIN_SETTINGS, - OTA_PACKAGE, - QUEUE + DASHBOARD(false, EntityType.DASHBOARD), + ASSET(false, EntityType.ASSET), + DEVICE(false, EntityType.DEVICE), + DEVICE_PROFILE(true, EntityType.DEVICE_PROFILE), + ASSET_PROFILE(true, EntityType.ASSET_PROFILE), + ENTITY_VIEW(false, EntityType.ENTITY_VIEW), + ALARM(false, EntityType.ALARM), + RULE_CHAIN(false, EntityType.RULE_CHAIN), + RULE_CHAIN_METADATA(false, null), + EDGE(false, EntityType.EDGE), + USER(true, EntityType.USER), + CUSTOMER(true, EntityType.CUSTOMER), + RELATION(true, null), + TENANT(true, EntityType.TENANT), + TENANT_PROFILE(true, EntityType.TENANT_PROFILE), + WIDGETS_BUNDLE(true, EntityType.WIDGETS_BUNDLE), + WIDGET_TYPE(true, EntityType.WIDGET_TYPE), + ADMIN_SETTINGS(true, null), + OTA_PACKAGE(true, EntityType.OTA_PACKAGE), + QUEUE(true, EntityType.QUEUE); + + private final boolean allEdgesRelated; + + private final EntityType entityType; + + + EdgeEventType(boolean allEdgesRelated, EntityType entityType) { + this.allEdgesRelated = allEdgesRelated; + this.entityType = entityType; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/AbstractRateLimitException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/AbstractRateLimitException.java new file mode 100644 index 0000000000..1d1da75da3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/AbstractRateLimitException.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2023 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.exception; + +public abstract class AbstractRateLimitException extends RuntimeException { + + public AbstractRateLimitException() { + super(); + } + + public AbstractRateLimitException(String message) { + super(message); + } + + public AbstractRateLimitException(String message, Throwable cause) { + super(message, cause); + } + + public AbstractRateLimitException(Throwable cause) { + super(cause); + } + + protected AbstractRateLimitException(String message, Throwable cause, + boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ApiUsageLimitsExceededException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ApiUsageLimitsExceededException.java index 2d24184a3f..aa9441c776 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ApiUsageLimitsExceededException.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ApiUsageLimitsExceededException.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.common.data.exception; -public class ApiUsageLimitsExceededException extends RuntimeException { +public class ApiUsageLimitsExceededException extends AbstractRateLimitException { public ApiUsageLimitsExceededException(String message) { super(message); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 3aaba1cf61..0cdf3ad1eb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -115,10 +115,6 @@ public class EntityIdFactory { return new DashboardId(uuid); case DEVICE: return new DeviceId(uuid); - case DEVICE_PROFILE: - return new DeviceProfileId(uuid); - case ASSET_PROFILE: - return new AssetProfileId(uuid); case ASSET: return new AssetId(uuid); case ALARM: @@ -131,12 +127,18 @@ public class EntityIdFactory { return new WidgetsBundleId(uuid); case WIDGET_TYPE: return new WidgetTypeId(uuid); + case DEVICE_PROFILE: + return new DeviceProfileId(uuid); + case ASSET_PROFILE: + return new AssetProfileId(uuid); + case TENANT_PROFILE: + return new TenantProfileId(uuid); case OTA_PACKAGE: return new OtaPackageId(uuid); - case QUEUE: - return new QueueId(uuid); case EDGE: return new EdgeId(uuid); + case QUEUE: + return new QueueId(uuid); } throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseDeleteTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseDeleteTsKvQuery.java index dee0e7aa9b..a97cc8f834 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseDeleteTsKvQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseDeleteTsKvQuery.java @@ -21,14 +21,20 @@ import lombok.Data; public class BaseDeleteTsKvQuery extends BaseTsKvQuery implements DeleteTsKvQuery { private final Boolean rewriteLatestIfDeleted; + private final Boolean deleteLatest; - public BaseDeleteTsKvQuery(String key, long startTs, long endTs, boolean rewriteLatestIfDeleted) { + public BaseDeleteTsKvQuery(String key, long startTs, long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) { super(key, startTs, endTs); this.rewriteLatestIfDeleted = rewriteLatestIfDeleted; + this.deleteLatest = deleteLatest; + } + + public BaseDeleteTsKvQuery(String key, long startTs, long endTs, boolean rewriteLatestIfDeleted) { + this(key, startTs, endTs, rewriteLatestIfDeleted, true); } public BaseDeleteTsKvQuery(String key, long startTs, long endTs) { - this(key, startTs, endTs, false); + this(key, startTs, endTs, false, true); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/DeleteTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DeleteTsKvQuery.java index 7b9b4ad16f..b2f41fffb4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/DeleteTsKvQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DeleteTsKvQuery.java @@ -19,4 +19,6 @@ public interface DeleteTsKvQuery extends TsKvQuery { Boolean getRewriteLatestIfDeleted(); + Boolean getDeleteLatest(); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java new file mode 100644 index 0000000000..23149c79c1 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java @@ -0,0 +1,108 @@ +/** + * Copyright © 2016-2023 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.msg; + +import lombok.Getter; +import org.thingsboard.server.common.data.StringUtils; + +import java.util.EnumSet; +import java.util.List; +import java.util.stream.Collectors; + +public enum TbMsgType { + + POST_ATTRIBUTES_REQUEST("Post attributes"), + POST_TELEMETRY_REQUEST("Post telemetry"), + TO_SERVER_RPC_REQUEST("RPC Request from Device"), + ACTIVITY_EVENT("Activity Event"), + INACTIVITY_EVENT("Inactivity Event"), + CONNECT_EVENT("Connect Event"), + DISCONNECT_EVENT("Disconnect Event"), + ENTITY_CREATED("Entity Created"), + ENTITY_UPDATED("Entity Updated"), + ENTITY_DELETED("Entity Deleted"), + ENTITY_ASSIGNED("Entity Assigned"), + ENTITY_UNASSIGNED("Entity Unassigned"), + ATTRIBUTES_UPDATED("Attributes Updated"), + ATTRIBUTES_DELETED("Attributes Deleted"), + ALARM, + ALARM_ACK("Alarm Acknowledged"), + ALARM_CLEAR("Alarm Cleared"), + ALARM_DELETE, + ALARM_ASSIGNED("Alarm Assigned"), + ALARM_UNASSIGNED("Alarm Unassigned"), + COMMENT_CREATED("Comment Created"), + COMMENT_UPDATED("Comment Updated"), + RPC_CALL_FROM_SERVER_TO_DEVICE("RPC Request to Device"), + ENTITY_ASSIGNED_FROM_TENANT("Entity Assigned From Tenant"), + ENTITY_ASSIGNED_TO_TENANT("Entity Assigned To Tenant"), + ENTITY_ASSIGNED_TO_EDGE, + ENTITY_UNASSIGNED_FROM_EDGE, + TIMESERIES_UPDATED("Timeseries Updated"), + TIMESERIES_DELETED("Timeseries Deleted"), + RPC_QUEUED("RPC Queued"), + RPC_SENT("RPC Sent"), + RPC_DELIVERED("RPC Delivered"), + RPC_SUCCESSFUL("RPC Successful"), + RPC_TIMEOUT("RPC Timeout"), + RPC_EXPIRED("RPC Expired"), + RPC_FAILED("RPC Failed"), + RPC_DELETED("RPC Deleted"), + RELATION_ADD_OR_UPDATE("Relation Added or Updated"), + RELATION_DELETED("Relation Deleted"), + RELATIONS_DELETED("All Relations Deleted"), + PROVISION_SUCCESS, + PROVISION_FAILURE, + SEND_EMAIL, + + // tellSelfOnly types + GENERATOR_NODE_SELF_MSG(null, true), + DEVICE_PROFILE_PERIODIC_SELF_MSG(null, true), + DEVICE_PROFILE_UPDATE_SELF_MSG(null, true), + DEVICE_UPDATE_SELF_MSG(null, true), + DEDUPLICATION_TIMEOUT_SELF_MSG(null, true), + DELAY_TIMEOUT_SELF_MSG(null, true), + MSG_COUNT_SELF_MSG(null, true), + + // Custom or N/A type: + NA; + + public static final List NODE_CONNECTIONS = EnumSet.allOf(TbMsgType.class).stream() + .filter(tbMsgType -> !tbMsgType.isTellSelfOnly()) + .map(TbMsgType::getRuleNodeConnection) + .filter(connection -> !TbNodeConnectionType.OTHER.equals(connection)) + .collect(Collectors.toUnmodifiableList()); + + @Getter + private final String ruleNodeConnection; + + @Getter + private final boolean tellSelfOnly; + + TbMsgType(String ruleNodeConnection, boolean tellSelfOnly) { + this.ruleNodeConnection = StringUtils.isNotEmpty(ruleNodeConnection) ? ruleNodeConnection : TbNodeConnectionType.OTHER; + this.tellSelfOnly = tellSelfOnly; + } + + TbMsgType(String ruleNodeConnection) { + this(ruleNodeConnection, false); + } + + TbMsgType() { + this(null, false); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java new file mode 100644 index 0000000000..584393bf53 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 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.msg; + +/** + * Created by ashvayka on 19.01.18. + */ +public final class TbNodeConnectionType { + + public static final String SUCCESS = "Success"; + public static final String FAILURE = "Failure"; + + public static final String TRUE = "True"; + public static final String FALSE = "False"; + + public static final String OTHER = "Other"; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java index 4a2c4657d5..4007a11071 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java @@ -24,7 +24,8 @@ public enum NotificationDeliveryMethod { WEB("web"), EMAIL("email"), SMS("SMS"), - SLACK("Slack"); + SLACK("Slack"), + MICROSOFT_TEAMS("Microsoft Teams"); @Getter private final String name; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java index 619e1ad38f..691ecf8bc1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java @@ -54,7 +54,6 @@ public class NotificationRequestStats { public void reportSent(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) { sent.computeIfAbsent(deliveryMethod, k -> new AtomicInteger()).incrementAndGet(); - processedRecipients.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipient.getId()); } public void reportError(NotificationDeliveryMethod deliveryMethod, Throwable error, NotificationRecipient recipient) { @@ -68,6 +67,10 @@ public class NotificationRequestStats { errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(recipient.getTitle(), errorMessage); } + public void reportProcessed(NotificationDeliveryMethod deliveryMethod, Object recipientId) { + processedRecipients.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipientId); + } + public boolean contains(NotificationDeliveryMethod deliveryMethod, Object recipientId) { Set processedRecipients = this.processedRecipients.get(deliveryMethod); return processedRecipients != null && processedRecipients.contains(recipientId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java new file mode 100644 index 0000000000..0ad7cc6d78 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2023 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.notification.settings; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; + +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Data +public class UserNotificationSettings { + + @NotNull + @Valid + private final Map prefs; + + public static final UserNotificationSettings DEFAULT = new UserNotificationSettings(Collections.emptyMap()); + + public static final Set deliveryMethods = NotificationTargetType.PLATFORM_USERS.getSupportedDeliveryMethods(); + + @JsonCreator + public UserNotificationSettings(@JsonProperty("prefs") Map prefs) { + this.prefs = prefs; + } + + public boolean isEnabled(NotificationType notificationType, NotificationDeliveryMethod deliveryMethod) { + NotificationPref pref = prefs.get(notificationType); + if (pref == null) { + return true; + } + if (!pref.isEnabled()) { + return false; + } + return pref.getEnabledDeliveryMethods().getOrDefault(deliveryMethod, true); + } + + @Data + public static class NotificationPref { + private boolean enabled; + @NotNull + private Map enabledDeliveryMethods; + + public static NotificationPref createDefault() { + NotificationPref pref = new NotificationPref(); + pref.setEnabled(true); + pref.setEnabledDeliveryMethods(deliveryMethods.stream().collect(Collectors.toMap(v -> v, v -> true))); + return pref; + } + + @JsonIgnore + @AssertTrue(message = "Only email, Web and SMS delivery methods are allowed") + public boolean isValid() { + return enabledDeliveryMethods.entrySet().stream() + .allMatch(entry -> deliveryMethods.contains(entry.getKey()) && entry.getValue() != null); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java new file mode 100644 index 0000000000..826f64bbd4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2023 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.notification.targets; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; + +@Data +@EqualsAndHashCode(callSuper = true) +public class MicrosoftTeamsNotificationTargetConfig extends NotificationTargetConfig implements NotificationRecipient { + + @NotBlank + private String webhookUrl; + @NotEmpty + private String channelName; + + @Override + public NotificationTargetType getType() { + return NotificationTargetType.MICROSOFT_TEAMS; + } + + @Override + public Object getId() { + return webhookUrl; + } + + @Override + public String getTitle() { + return channelName; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationRecipient.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationRecipient.java index 940d596332..3fbe7173fd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationRecipient.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationRecipient.java @@ -21,10 +21,16 @@ public interface NotificationRecipient { String getTitle(); - String getFirstName(); + default String getFirstName() { + return null; + } - String getLastName(); + default String getLastName() { + return null; + } - String getEmail(); + default String getEmail() { + return null; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java index 231e419fb0..529167c637 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java @@ -30,7 +30,8 @@ import org.thingsboard.server.common.data.validation.NoXss; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @Type(value = PlatformUsersNotificationTargetConfig.class, name = "PLATFORM_USERS"), - @Type(value = SlackNotificationTargetConfig.class, name = "SLACK") + @Type(value = SlackNotificationTargetConfig.class, name = "SLACK"), + @Type(value = MicrosoftTeamsNotificationTargetConfig.class, name = "MICROSOFT_TEAMS") }) @Data public abstract class NotificationTargetConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java index 1254654ecf..4995551d01 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java @@ -26,7 +26,8 @@ import java.util.Set; public enum NotificationTargetType { PLATFORM_USERS(Set.of(NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS)), - SLACK(Set.of(NotificationDeliveryMethod.SLACK)); + SLACK(Set.of(NotificationDeliveryMethod.SLACK)), + MICROSOFT_TEAMS(Set.of(NotificationDeliveryMethod.MICROSOFT_TEAMS)); @Getter private final Set supportedDeliveryMethods; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java index ecb7f2d2b6..30316ef33b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java @@ -22,10 +22,10 @@ import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Data; import lombok.NoArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import javax.validation.constraints.NotEmpty; +import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method") @@ -33,7 +33,8 @@ import javax.validation.constraints.NotEmpty; @Type(name = "WEB", value = WebDeliveryMethodNotificationTemplate.class), @Type(name = "EMAIL", value = EmailDeliveryMethodNotificationTemplate.class), @Type(name = "SMS", value = SmsDeliveryMethodNotificationTemplate.class), - @Type(name = "SLACK", value = SlackDeliveryMethodNotificationTemplate.class) + @Type(name = "SLACK", value = SlackDeliveryMethodNotificationTemplate.class), + @Type(name = "MICROSOFT_TEAMS", value = MicrosoftTeamsDeliveryMethodNotificationTemplate.class) }) @Data @NoArgsConstructor @@ -41,7 +42,7 @@ public abstract class DeliveryMethodNotificationTemplate { private boolean enabled; @NotEmpty - private String body; + protected String body; public DeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) { this.enabled = other.enabled; @@ -54,8 +55,7 @@ public abstract class DeliveryMethodNotificationTemplate { @JsonIgnore public abstract DeliveryMethodNotificationTemplate copy(); - public boolean containsAny(String... params) { - return StringUtils.containsAny(body, params); - } + @JsonIgnore + public abstract List getTemplatableValues(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java index fe909a104e..4ac2e1fb17 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java @@ -19,12 +19,12 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; -import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.constraints.NotEmpty; +import java.util.List; @Data @NoArgsConstructor @@ -37,6 +37,11 @@ public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotif @NotEmpty private String subject; + private final List templatableValues = List.of( + TemplatableValue.of(this::getBody, this::setBody), + TemplatableValue.of(this::getSubject, this::setSubject) + ); + public EmailDeliveryMethodNotificationTemplate(EmailDeliveryMethodNotificationTemplate other) { super(other); this.subject = other.subject; @@ -52,9 +57,4 @@ public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotif return new EmailDeliveryMethodNotificationTemplate(this); } - @Override - public boolean containsAny(String... params) { - return super.containsAny(params) || StringUtils.containsAny(subject, params); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java new file mode 100644 index 0000000000..4a3df6287e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java @@ -0,0 +1,90 @@ +/** + * Copyright © 2016-2023 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.notification.template; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; + +import java.util.List; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class MicrosoftTeamsDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { + + private String subject; + private String themeColor; + private Button button; + + private final List templatableValues = List.of( + TemplatableValue.of(this::getBody, this::setBody), + TemplatableValue.of(this::getSubject, this::setSubject), + TemplatableValue.of(() -> button != null ? button.getText() : null, + processed -> { if (button != null) button.setText(processed); }), + TemplatableValue.of(() -> button != null ? button.getLink() : null, + processed -> { if (button != null) button.setLink(processed); }) + ); + + public MicrosoftTeamsDeliveryMethodNotificationTemplate(MicrosoftTeamsDeliveryMethodNotificationTemplate other) { + super(other); + this.subject = other.subject; + this.themeColor = other.themeColor; + this.button = other.button != null ? new Button(other.button) : null; + } + + @Override + public NotificationDeliveryMethod getMethod() { + return NotificationDeliveryMethod.MICROSOFT_TEAMS; + } + + @Override + public MicrosoftTeamsDeliveryMethodNotificationTemplate copy() { + return new MicrosoftTeamsDeliveryMethodNotificationTemplate(this); + } + + @Data + @NoArgsConstructor + public static class Button { + private boolean enabled; + private String text; + private LinkType linkType; + private String link; + + private UUID dashboardId; + private String dashboardState; + private boolean setEntityIdInState; + + public Button(Button other) { + this.enabled = other.enabled; + this.text = other.text; + this.linkType = other.linkType; + this.link = other.link; + this.dashboardId = other.dashboardId; + this.dashboardState = other.dashboardState; + this.setEntityIdInState = other.setEntityIdInState; + } + + public enum LinkType { + LINK, DASHBOARD + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java index 8457677308..6a3790feb6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java @@ -22,12 +22,18 @@ import lombok.ToString; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.validation.NoXss; +import java.util.List; + @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class SlackDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate { + private final List templatableValues = List.of( + TemplatableValue.of(this::getBody, this::setBody) + ); + public SlackDeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) { super(other); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java index 7dc2e494f3..f4aab10e1e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java @@ -23,12 +23,18 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.util.List; + @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class SmsDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate { + private final List templatableValues = List.of( + TemplatableValue.of(this::getBody, this::setBody) + ); + public SmsDeliveryMethodNotificationTemplate(SmsDeliveryMethodNotificationTemplate other) { super(other); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/TemplatableValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/TemplatableValue.java new file mode 100644 index 0000000000..25dfeb643a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/TemplatableValue.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2023 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.notification.template; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.function.Consumer; +import java.util.function.Supplier; + +@RequiredArgsConstructor +public class TemplatableValue { + private final Supplier getter; + private final Consumer setter; + + public static TemplatableValue of(Supplier getter, Consumer setter) { + return new TemplatableValue(getter, setter); + } + + public String get() { + return getter.get(); + } + + public void set(String processed) { + setter.accept(processed); + } + + public boolean containsParams(Collection params) { + return StringUtils.containsAny(get(), params.toArray(String[]::new)); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java index f26ed00568..f45371f716 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java @@ -23,12 +23,12 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; -import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.constraints.NotEmpty; +import java.util.List; import java.util.Optional; @Data @@ -43,6 +43,13 @@ public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotific private String subject; private JsonNode additionalConfig; + private final List templatableValues = List.of( + TemplatableValue.of(this::getBody, this::setBody), + TemplatableValue.of(this::getSubject, this::setSubject), + TemplatableValue.of(this::getButtonText, this::setButtonText), + TemplatableValue.of(this::getButtonLink, this::setButtonLink) + ); + public WebDeliveryMethodNotificationTemplate(WebDeliveryMethodNotificationTemplate other) { super(other); this.subject = other.subject; @@ -107,10 +114,4 @@ public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotific return new WebDeliveryMethodNotificationTemplate(this); } - @Override - public boolean containsAny(String... params) { - return super.containsAny(params) || StringUtils.containsAny(subject, params) - || StringUtils.containsAny(getButtonText(), params) || StringUtils.containsAny(getButtonLink(), params); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java index 2ae94a0064..6eb9218956 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java @@ -20,13 +20,16 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import java.io.Serializable; import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; @ApiModel -public class PageData { +public class PageData implements Serializable { + + public static final PageData EMPTY_PAGE_DATA = new PageData<>(); private final List data; private final int totalPages; @@ -48,6 +51,11 @@ public class PageData { this.hasNext = hasNext; } + @SuppressWarnings("unchecked") + public static PageData emptyPageData() { + return (PageData) EMPTY_PAGE_DATA; + } + @ApiModelProperty(position = 1, value = "Array of the entities", accessMode = ApiModelProperty.AccessMode.READ_ONLY) public List getData() { return data; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java index 5c540995aa..62a6377519 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java @@ -36,4 +36,6 @@ public class SortOrder { ASC, DESC } + public static final SortOrder BY_CREATED_TIME_DESC = new SortOrder("createdTime", Direction.DESC); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategy.java index e1889ed759..4ad8265c22 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategy.java @@ -17,8 +17,13 @@ package org.thingsboard.server.common.data.queue; import lombok.Data; +import java.io.Serializable; + @Data -public class ProcessingStrategy { +public class ProcessingStrategy implements Serializable { + + private static final long serialVersionUID = 293598327194813L; + private ProcessingStrategyType type; private int retries; private double failurePercentage; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java index 6839c60bcd..5b613a7c05 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.queue; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.HasName; @@ -25,6 +27,8 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfi import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.util.Optional; + @Data public class Queue extends BaseDataWithAdditionalInfo implements HasName, HasTenantId { private TenantId tenantId; @@ -60,4 +64,13 @@ public class Queue extends BaseDataWithAdditionalInfo implements HasNam this.processingStrategy = queueConfiguration.getProcessingStrategy(); setAdditionalInfo(queueConfiguration.getAdditionalInfo()); } -} \ No newline at end of file + + + @JsonIgnore + public String getCustomProperties() { + return Optional.ofNullable(getAdditionalInfo()) + .map(info -> info.get("customProperties")) + .filter(JsonNode::isTextual).map(JsonNode::asText).orElse(null); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategy.java index 18198ee6d3..63461951c4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategy.java @@ -17,8 +17,13 @@ package org.thingsboard.server.common.data.queue; import lombok.Data; +import java.io.Serializable; + @Data -public class SubmitStrategy { +public class SubmitStrategy implements Serializable { + + private static final long serialVersionUID = 2938457983298400L; + private SubmitStrategyType type; private int batchSize; } \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java index b19dbbbaee..cd627821ac 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java @@ -19,7 +19,7 @@ import lombok.Getter; public enum UserSettingsType { - GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS, GETTING_STARTED; + GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS, GETTING_STARTED, NOTIFICATIONS; @Getter private final boolean reserved; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index 23a98b395b..92c6a332df 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; import java.lang.annotation.ElementType; @@ -52,6 +53,7 @@ import java.lang.annotation.Target; @Type(name = "CUSTOMER", value = Customer.class), @Type(name = "ENTITY_VIEW", value = EntityView.class), @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundle.class), + @Type(name = "WIDGET_TYPE", value = WidgetTypeDetails.class), @Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class), @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java index 55a684df04..5ee856e312 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java @@ -30,13 +30,6 @@ import java.util.List; public class WidgetsBundleExportData extends EntityExportData { @JsonProperty(index = 3) - private List widgets; - - @Override - public EntityExportData sort() { - super.sort(); - widgets.sort(Comparator.comparing(BaseWidgetType::getAlias)); - return this; - } + private List widgets; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index d84c87ac51..bd2b80f10a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.TenantProfileType; @Data public class DefaultTenantProfileConfiguration implements TenantProfileConfiguration { + private static final long serialVersionUID = -7134932690332578595L; + private long maxDevices; private long maxAssets; private long maxCustomers; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java index dacff693b7..a30be83b76 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java @@ -22,6 +22,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.TenantProfileType; +import java.io.Serializable; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -29,7 +31,7 @@ import org.thingsboard.server.common.data.TenantProfileType; property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = DefaultTenantProfileConfiguration.class, name = "DEFAULT")}) -public interface TenantProfileConfiguration { +public interface TenantProfileConfiguration extends Serializable { @JsonIgnore TenantProfileType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java index 1def815cff..a863b34dfa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java @@ -19,11 +19,14 @@ import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import java.io.Serializable; import java.util.List; @ApiModel @Data -public class TenantProfileData { +public class TenantProfileData implements Serializable { + + private static final long serialVersionUID = -3642550257035920976L; @ApiModelProperty(position = 1, value = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.") private TenantProfileConfiguration configuration; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileQueueConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileQueueConfiguration.java index d73e7fd486..46eaa82743 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileQueueConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileQueueConfiguration.java @@ -20,8 +20,13 @@ import lombok.Data; import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategy; +import java.io.Serializable; + @Data -public class TenantProfileQueueConfiguration { +public class TenantProfileQueueConfiguration implements Serializable { + + private static final long serialVersionUID = -546600745123197362L; + private String name; private String topic; private int pollInterval; 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 a7e9fee82a..8fd0e58006 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 @@ -33,18 +33,17 @@ public class BaseWidgetType extends BaseData implements HasName, H @ApiModelProperty(position = 3, value = "JSON object with Tenant Id.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private TenantId tenantId; @NoXss - @Length(fieldName = "bundleAlias") - @ApiModelProperty(position = 4, value = "Reference to widget bundle", accessMode = ApiModelProperty.AccessMode.READ_ONLY) - private String bundleAlias; - @NoXss - @Length(fieldName = "alias") - @ApiModelProperty(position = 5, value = "Unique alias that is used in dashboards as a reference widget type", accessMode = ApiModelProperty.AccessMode.READ_ONLY) - private String alias; + @Length(fieldName = "fqn") + @ApiModelProperty(position = 5, value = "Unique FQN that is used in dashboards as a reference widget type", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private String fqn; @NoXss @Length(fieldName = "name") @ApiModelProperty(position = 6, value = "Widget name used in search and UI", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String name; + @ApiModelProperty(position = 7, value = "Whether widget type is deprecated.", example = "true") + private boolean deprecated; + public BaseWidgetType() { super(); } @@ -56,9 +55,9 @@ public class BaseWidgetType extends BaseData implements HasName, H public BaseWidgetType(BaseWidgetType widgetType) { super(widgetType); this.tenantId = widgetType.getTenantId(); - this.bundleAlias = widgetType.getBundleAlias(); - this.alias = widgetType.getAlias(); + this.fqn = widgetType.getFqn(); this.name = widgetType.getName(); + this.deprecated = widgetType.isDeprecated(); } @ApiModelProperty(position = 1, value = "JSON object with the Widget Type Id. " + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java index f726cfb71a..63c270851c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.WidgetTypeId; @Data public class WidgetType extends BaseWidgetType { - @ApiModelProperty(position = 7, value = "Complex JSON object that describes the widget type", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @ApiModelProperty(position = 8, value = "Complex JSON object that describes the widget type", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private transient JsonNode descriptor; public WidgetType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java index e6faece85d..9ebc0b6639 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java @@ -18,22 +18,31 @@ package org.thingsboard.server.common.data.widget; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @Data -@JsonPropertyOrder({ "alias", "name", "image", "description", "descriptor" }) -public class WidgetTypeDetails extends WidgetType { +@JsonPropertyOrder({ "fqn", "name", "deprecated", "image", "description", "descriptor", "externalId" }) +public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, ExportableEntity { @Length(fieldName = "image", max = 1000000) - @ApiModelProperty(position = 8, value = "Base64 encoded thumbnail", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @ApiModelProperty(position = 9, value = "Base64 encoded thumbnail", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String image; @NoXss @Length(fieldName = "description") - @ApiModelProperty(position = 9, value = "Description of the widget", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @ApiModelProperty(position = 10, value = "Description of the widget", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String description; + @Getter + @Setter + private WidgetTypeId externalId; + public WidgetTypeDetails() { super(); } @@ -50,5 +59,6 @@ public class WidgetTypeDetails extends WidgetType { super(widgetTypeDetails); this.image = widgetTypeDetails.getImage(); this.description = widgetTypeDetails.getDescription(); + this.externalId = widgetTypeDetails.getExternalId(); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java index f5c4abd66d..0e8bce0a19 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java @@ -23,13 +23,13 @@ import org.thingsboard.server.common.data.validation.NoXss; @Data public class WidgetTypeInfo extends BaseWidgetType { - @ApiModelProperty(position = 7, value = "Base64 encoded widget thumbnail", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @ApiModelProperty(position = 8, value = "Base64 encoded widget thumbnail", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String image; @NoXss - @ApiModelProperty(position = 7, value = "Description of the widget type", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @ApiModelProperty(position = 9, value = "Description of the widget type", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String description; @NoXss - @ApiModelProperty(position = 8, value = "Type of the widget (timeseries, latest, control, alarm or static)", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @ApiModelProperty(position = 10, value = "Type of the widget (timeseries, latest, control, alarm or static)", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String widgetType; public WidgetTypeInfo() { @@ -50,4 +50,15 @@ public class WidgetTypeInfo extends BaseWidgetType { this.description = widgetTypeInfo.getDescription(); this.widgetType = widgetTypeInfo.getWidgetType(); } + + public WidgetTypeInfo(WidgetTypeDetails widgetTypeDetails) { + super(widgetTypeDetails); + this.image = widgetTypeDetails.getImage(); + this.description = widgetTypeDetails.getDescription(); + if (widgetTypeDetails.getDescriptor() != null && widgetTypeDetails.getDescriptor().has("type")) { + this.widgetType = widgetTypeDetails.getDescriptor().get("type").asText(); + } else { + this.widgetType = ""; + } + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundleWidget.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundleWidget.java new file mode 100644 index 0000000000..1ac3c19132 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundleWidget.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 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.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class WidgetsBundleWidget { + + private WidgetsBundleId widgetsBundleId; + private WidgetTypeId widgetTypeId; + private int widgetTypeOrder; + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/EntityTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/EntityTypeTest.java new file mode 100644 index 0000000000..9eee9ec23d --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/EntityTypeTest.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 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; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class EntityTypeTest { + + // backward-compatibility test + @Test + void getNormalNameTest() { + assertThat(EntityType.ENTITY_VIEW.getNormalName()).isEqualTo("Entity View"); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/StringUtilsTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/StringUtilsTest.java new file mode 100644 index 0000000000..4cf21baa99 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/StringUtilsTest.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2023 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; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class StringUtilsTest { + + @ParameterizedTest + @ValueSource(strings = { + "\000", "\u0000", " \000", " \000 ", "\000 ", "\000\000", "\000 \000", + "世\000界", "F0929906\000\000\000\000\000\000\000\000\000", + }) + void testContains0x00_thenTrue(String sample) { + assertThat(StringUtils.contains0x00(sample)).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "abc", "世界", "\001", "\uD83D\uDC0C"}) + void testContains0x00_thenFalse(String sample) { + assertThat(StringUtils.contains0x00(sample)).isFalse(); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java new file mode 100644 index 0000000000..8c602c3cff --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2023 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.audit; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.audit.ActionType.ACTIVATED; +import static org.thingsboard.server.common.data.audit.ActionType.ATTRIBUTES_READ; +import static org.thingsboard.server.common.data.audit.ActionType.CREDENTIALS_READ; +import static org.thingsboard.server.common.data.audit.ActionType.CREDENTIALS_UPDATED; +import static org.thingsboard.server.common.data.audit.ActionType.DELETED_COMMENT; +import static org.thingsboard.server.common.data.audit.ActionType.LOCKOUT; +import static org.thingsboard.server.common.data.audit.ActionType.LOGIN; +import static org.thingsboard.server.common.data.audit.ActionType.LOGOUT; +import static org.thingsboard.server.common.data.audit.ActionType.RPC_CALL; +import static org.thingsboard.server.common.data.audit.ActionType.SMS_SENT; +import static org.thingsboard.server.common.data.audit.ActionType.SUSPENDED; + +class ActionTypeTest { + + private static final List typesWithNullRuleEngineMsgType = List.of( + RPC_CALL, + CREDENTIALS_UPDATED, + ACTIVATED, + SUSPENDED, + CREDENTIALS_READ, + ATTRIBUTES_READ, + LOGIN, + LOGOUT, + LOCKOUT, + DELETED_COMMENT, + SMS_SENT + ); + + // backward-compatibility tests + + @Test + void getRuleEngineMsgTypeTest() { + var types = ActionType.values(); + for (var type : types) { + if (typesWithNullRuleEngineMsgType.contains(type)) { + assertThat(type.getRuleEngineMsgType()).isEmpty(); + } else { + assertThat(type.getRuleEngineMsgType()).isPresent(); + } + } + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java new file mode 100644 index 0000000000..870d5a2804 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2023 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.msg; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE; +import static org.thingsboard.server.common.data.msg.TbMsgType.NA; +import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_ASSIGNED_TO_EDGE; +import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE; +import static org.thingsboard.server.common.data.msg.TbMsgType.MSG_COUNT_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.PROVISION_FAILURE; +import static org.thingsboard.server.common.data.msg.TbMsgType.PROVISION_SUCCESS; +import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_UPDATE_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.GENERATOR_NODE_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.SEND_EMAIL; + +class TbMsgTypeTest { + + private static final List typesWithNullRuleNodeConnection = List.of( + ALARM, + ALARM_DELETE, + ENTITY_ASSIGNED_TO_EDGE, + ENTITY_UNASSIGNED_FROM_EDGE, + PROVISION_FAILURE, + PROVISION_SUCCESS, + SEND_EMAIL, + GENERATOR_NODE_SELF_MSG, + DEVICE_PROFILE_PERIODIC_SELF_MSG, + DEVICE_PROFILE_UPDATE_SELF_MSG, + DEVICE_UPDATE_SELF_MSG, + DEDUPLICATION_TIMEOUT_SELF_MSG, + DELAY_TIMEOUT_SELF_MSG, + MSG_COUNT_SELF_MSG, + NA + ); + + // backward-compatibility tests + + @Test + void getRuleNodeConnectionsTest() { + var tbMsgTypes = TbMsgType.values(); + for (var type : tbMsgTypes) { + if (typesWithNullRuleNodeConnection.contains(type)) { + assertThat(type.getRuleNodeConnection()).isEqualTo(TbNodeConnectionType.OTHER); + } else { + assertThat(type.getRuleNodeConnection()).isNotEqualTo(TbNodeConnectionType.OTHER); + } + } + } + + @Test + void getRuleNodeConnectionOrElseOtherTest() { + var tbMsgTypes = TbMsgType.values(); + for (var type : tbMsgTypes) { + if (typesWithNullRuleNodeConnection.contains(type)) { + assertThat(type.getRuleNodeConnection()) + .isEqualTo(TbNodeConnectionType.OTHER); + } else { + assertThat(type.getRuleNodeConnection()).isNotNull() + .isNotEqualTo(TbNodeConnectionType.OTHER); + } + } + } + +} diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index c99a475236..60562efbf2 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 4e8548aa33..dcd543a9a6 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -221,17 +221,11 @@ public class EdgeGrpcClient implements EdgeRpcClient { } @Override - public void sendSyncRequestMsg(boolean syncRequired) { - sendSyncRequestMsg(syncRequired, true); - } - - @Override - public void sendSyncRequestMsg(boolean syncRequired, boolean fullSync) { + public void sendSyncRequestMsg(boolean fullSyncRequired) { uplinkMsgLock.lock(); try { SyncRequestMsg syncRequestMsg = SyncRequestMsg.newBuilder() - .setSyncRequired(syncRequired) - .setFullSync(fullSync) + .setFullSync(fullSyncRequired) .build(); this.inputStream.onNext(RequestMsg.newBuilder() .setMsgType(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE) diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java index 44d00e22a8..9f2a303f5b 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java @@ -34,9 +34,7 @@ public interface EdgeRpcClient { void disconnect(boolean onError) throws InterruptedException; - void sendSyncRequestMsg(boolean syncRequired); - - void sendSyncRequestMsg(boolean syncRequired, boolean fullSync); + void sendSyncRequestMsg(boolean fullSyncRequired); void sendUplinkMsg(UplinkMsg uplinkMsg); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index afcf4056c4..3e8310222b 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -85,7 +85,7 @@ message ConnectResponseMsg { } message SyncRequestMsg { - bool syncRequired = 1; + bool syncRequired = 1; // deprecated optional bool fullSync = 2; } @@ -187,6 +187,9 @@ message DashboardUpdateMsg { string title = 6; string configuration = 7; optional string assignedCustomers = 8; + optional string image = 9; + optional int32 mobileOrder = 10; + bool mobileHide = 11; } message DeviceUpdateMsg { @@ -355,19 +358,22 @@ message WidgetsBundleUpdateMsg { optional bytes image = 6; bool isSystem = 7; optional string description = 8; + optional string widgets = 9; } message WidgetTypeUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - optional string bundleAlias = 4; - optional string alias = 5; + optional string bundleAlias = 4; // deprecated + optional string alias = 5; // deprecated optional string name = 6; optional string descriptorJson = 7; bool isSystem = 8; optional string image = 9; optional string description = 10; + optional string fqn = 11; + bool deprecated = 12; } message AdminSettingsUpdateMsg { @@ -383,6 +389,36 @@ message UserCredentialsUpdateMsg { string password = 4; } +message TenantUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string title = 4; + int64 profileIdMSB = 5; + int64 profileIdLSB = 6; + string region = 7; + optional string country = 8; + optional string state = 9; + optional string city = 10; + optional string address = 11; + optional string address2 = 12; + optional string zip = 13; + optional string phone = 14; + optional string email = 15; + optional string additionalInfo = 16; +} + +message TenantProfileUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string name = 4; + optional string description = 5; + bool default = 6; + bool isolatedRuleChain = 7; + bytes profileDataBytes = 8; +} + message RuleChainMetadataRequestMsg { int64 ruleChainIdMSB = 1; int64 ruleChainIdLSB = 2; @@ -529,6 +565,11 @@ message UplinkMsg { repeated DeviceProfileDevicesRequestMsg deviceProfileDevicesRequestMsg = 13; // deprecated repeated WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg = 14; repeated EntityViewsRequestMsg entityViewsRequestMsg = 15; + repeated AssetUpdateMsg assetUpdateMsg = 16; + repeated DashboardUpdateMsg dashboardUpdateMsg = 17; + repeated EntityViewUpdateMsg entityViewUpdateMsg = 18; + repeated AssetProfileUpdateMsg assetProfileUpdateMsg = 19; + repeated DeviceProfileUpdateMsg deviceProfileUpdateMsg = 20; } message UplinkResponseMsg { @@ -569,5 +610,7 @@ message DownlinkMsg { repeated QueueUpdateMsg queueUpdateMsg = 23; repeated AssetProfileUpdateMsg assetProfileUpdateMsg = 24; EdgeConfiguration edgeConfiguration = 25; + repeated TenantUpdateMsg tenantUpdateMsg = 26; + repeated TenantProfileUpdateMsg tenantProfileUpdateMsg = 27; } diff --git a/common/message/pom.xml b/common/message/pom.xml index 62e176e675..e64d4676ce 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 17bc7c0a8f..b107d3d06e 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -29,10 +29,12 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.gen.MsgProtos; import org.thingsboard.server.common.msg.queue.TbMsgCallback; import java.io.Serializable; +import java.util.Objects; import java.util.UUID; /** @@ -42,10 +44,15 @@ import java.util.UUID; @Slf4j public final class TbMsg implements Serializable { + public static final String EMPTY_JSON_OBJECT = "{}"; + public static final String EMPTY_JSON_ARRAY = "[]"; + public static final String EMPTY_STRING = ""; + private final String queueName; private final UUID id; private final long ts; private final String type; + private final TbMsgType internalType; private final EntityId originator; private final CustomerId customerId; private final TbMsgMetaData metaData; @@ -66,98 +73,255 @@ public final class TbMsg implements Serializable { return ctx.getAndIncrementRuleNodeCounter(); } + @Deprecated(since = "3.6.0", forRemoval = true) public static TbMsg newMsg(String queueName, String type, EntityId originator, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { return newMsg(queueName, type, originator, null, metaData, data, ruleChainId, ruleNodeId); } + /** + * Creates a new TbMsg instance with the specified parameters. + * + *

Deprecated: This method is deprecated since version 3.6.0 and should only be used when you need to + * specify a custom message type that doesn't exist in the {@link TbMsgType} enum. For standard message types, + * it is recommended to use the {@link #newMsg(String, TbMsgType, EntityId, CustomerId, TbMsgMetaData, String, RuleChainId, RuleNodeId)} + * method instead.

+ * + * @param queueName the name of the queue where the message will be sent + * @param type the type of the message + * @param originator the originator of the message + * @param customerId the ID of the customer associated with the message + * @param metaData the metadata of the message + * @param data the data of the message + * @param ruleChainId the ID of the rule chain associated with the message + * @param ruleNodeId the ID of the rule node associated with the message + * @return new TbMsg instance + */ + @Deprecated(since = "3.6.0") public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); } + @Deprecated(since = "3.6.0", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data) { return newMsg(type, originator, null, metaData, data); } + @Deprecated(since = "3.6.0", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId, + metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); + } + + public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { + return newMsg(queueName, type, originator, null, metaData, data, ruleChainId, ruleNodeId); + } + + public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); + } + + public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { + return newMsg(type, originator, null, metaData, data); + } + + public static TbMsg newMsg(TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } // REALLY NEW MSG + /** + * Creates a new TbMsg instance with the specified parameters. + * + *

Deprecated: This method is deprecated since version 3.6.0 and should only be used when you need to + * specify a custom message type that doesn't exist in the {@link TbMsgType} enum. For standard message types, + * it is recommended to use the {@link #newMsg(String, TbMsgType, EntityId, TbMsgMetaData, String)} + * method instead.

+ * + * @param queueName the name of the queue where the message will be sent + * @param type the type of the message + * @param originator the originator of the message + * @param metaData the metadata of the message + * @param data the data of the message + * @return new TbMsg instance + */ + @Deprecated(since = "3.6.0") public static TbMsg newMsg(String queueName, String type, EntityId originator, TbMsgMetaData metaData, String data) { return newMsg(queueName, type, originator, null, metaData, data); } + /** + * Creates a new TbMsg instance with the specified parameters. + * + *

Deprecated: This method is deprecated since version 3.6.0 and should only be used when you need to + * specify a custom message type that doesn't exist in the {@link TbMsgType} enum. For standard message types, + * it is recommended to use the {@link #newMsg(String, TbMsgType, EntityId, CustomerId, TbMsgMetaData, String)} + * method instead.

+ * + * @param queueName the name of the queue where the message will be sent + * @param type the type of the message + * @param originator the originator of the message + * @param customerId the ID of the customer associated with the message + * @param metaData the metadata of the message + * @param data the data of the message + * @return new TbMsg instance + */ + @Deprecated(since = "3.6.0") public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { - return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } + @Deprecated(since = "3.6.0", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { - return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId, metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY); } + /** + * Creates a new TbMsg instance with the specified parameters. + * + *

Deprecated: This method is deprecated since version 3.6.0 and should only be used when you need to + * specify a custom message type that doesn't exist in the {@link TbMsgType} enum. For standard message types, + * it is recommended to use the {@link #newMsg(TbMsgType, EntityId, TbMsgMetaData, TbMsgDataType, String)} + * method instead.

+ * + * @param type the type of the message + * @param originator the originator of the message + * @param metaData the metadata of the message + * @param dataType the dataType of the message + * @param data the data of the message + * @return new TbMsg instance + */ + @Deprecated(since = "3.6.0") public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { return newMsg(type, originator, null, metaData, dataType, data); } + public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { + return newMsg(queueName, type, originator, null, metaData, data); + } + + public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); + } + + public static TbMsg newMsg(TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY); + } + + public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { + return newMsg(type, originator, null, metaData, dataType, data); + } + // For Tests only + @Deprecated(since = "3.6.0", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, null, metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); } + @Deprecated(since = "3.6.0", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) { - return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, null, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback); } + /** + * Transforms an existing TbMsg instance by changing its message type, originator, metadata, and data. + * + *

Deprecated: This method is deprecated since version 3.6.0 and should only be used when you need to + * specify a custom message type that doesn't exist in the {@link TbMsgType} enum. For standard message types, + * it is recommended to use the {@link #transformMsg(TbMsg, TbMsgType, EntityId, TbMsgMetaData, String)} + * method instead.

+ * + * + * @param tbMsg the TbMsg instance to transform + * @param type the new message type + * @param originator the new originator + * @param metaData the new metadata + * @param data the new data + * @return the transformed TbMsg instance + */ + @Deprecated(since = "3.6.0") public static TbMsg transformMsg(TbMsg tbMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) { + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, null, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, + data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); + } + + public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, + metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); + } + + public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) { + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, + metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback); + } + + public static TbMsg transformMsg(TbMsg tbMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); } + public static TbMsg transformMsgOriginator(TbMsg tbMsg, EntityId originatorId) { + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, originatorId, tbMsg.getCustomerId(), tbMsg.metaData, tbMsg.dataType, + tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); + } + public static TbMsg transformMsgData(TbMsg tbMsg, String data) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } - public static TbMsg transformMsg(TbMsg tbMsg, TbMsgMetaData metadata) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType, + public static TbMsg transformMsgMetadata(TbMsg tbMsg, TbMsgMetaData metadata) { + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType, tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } - public static TbMsg transformMsg(TbMsg tbMsg, CustomerId customerId) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType, + public static TbMsg transformMsg(TbMsg tbMsg, TbMsgMetaData metadata, String data) { + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata, tbMsg.dataType, + data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); + } + + public static TbMsg transformMsgCustomerId(TbMsg tbMsg, CustomerId customerId) { + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } - public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, + public static TbMsg transformMsgRuleChainId(TbMsg tbMsg, RuleChainId ruleChainId) { + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); } - public static TbMsg transformMsg(TbMsg tbMsg, String queueName) { - return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, + public static TbMsg transformMsgQueueName(TbMsg tbMsg, String queueName) { + return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId, String queueName) { - return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, + return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); } //used for enqueueForTellNext public static TbMsg newMsg(TbMsg tbMsg, String queueName, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(), + return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getInternalType(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(), tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.ctx.copy(), TbMsgCallback.EMPTY); } - private TbMsg(String queueName, UUID id, long ts, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, + private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, + RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) { + this(queueName, id, ts, internalType, internalType.name(), originator, customerId, metaData, dataType, data, ruleChainId, ruleNodeId, ctx, callback); + } + + private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) { this.id = id; this.queueName = queueName; @@ -167,6 +331,7 @@ public final class TbMsg implements Serializable { this.ts = System.currentTimeMillis(); } this.type = type; + this.internalType = internalType != null ? internalType : getInternalType(type); this.originator = originator; if (customerId == null || customerId.isNullUid()) { if (originator != null && originator.getEntityType() == EntityType.CUSTOMER) { @@ -183,11 +348,7 @@ public final class TbMsg implements Serializable { this.ruleChainId = ruleChainId; this.ruleNodeId = ruleNodeId; this.ctx = ctx != null ? ctx : new TbMsgProcessingCtx(); - if (callback != null) { - this.callback = callback; - } else { - this.callback = TbMsgCallback.EMPTY; - } + this.callback = Objects.requireNonNullElse(callback, TbMsgCallback.EMPTY); } public static ByteString toByteString(TbMsg msg) { @@ -256,7 +417,7 @@ public final class TbMsg implements Serializable { } TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()]; - return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), proto.getType(), entityId, customerId, + return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), null, proto.getType(), entityId, customerId, metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, ctx, callback); } catch (InvalidProtocolBufferException e) { throw new IllegalStateException("Could not parse protobuf for TbMsg", e); @@ -268,27 +429,23 @@ public final class TbMsg implements Serializable { } public TbMsg copyWithRuleChainId(RuleChainId ruleChainId, UUID msgId) { - return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId, + return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId, this.metaData, this.dataType, this.data, ruleChainId, null, this.ctx, callback); } public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID msgId) { - return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId, + return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback); } public TbMsg copyWithNewCtx() { - return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.customerId, + return new TbMsg(this.queueName, this.id, this.ts, this.internalType, this.type, this.originator, this.customerId, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx.copy(), TbMsgCallback.EMPTY); } public TbMsgCallback getCallback() { // May be null in case of deserialization; - if (callback != null) { - return callback; - } else { - return TbMsgCallback.EMPTY; - } + return Objects.requireNonNullElse(callback, TbMsgCallback.EMPTY); } public void pushToStack(RuleChainId ruleChainId, RuleNodeId ruleNodeId) { @@ -317,4 +474,26 @@ public final class TbMsg implements Serializable { } return ts; } + + private TbMsgType getInternalType(String type) { + try { + return TbMsgType.valueOf(type); + } catch (IllegalArgumentException e) { + return TbMsgType.NA; + } + } + + public boolean isTypeOf(TbMsgType tbMsgType) { + return internalType.equals(tbMsgType); + } + + public boolean isTypeOneOf(TbMsgType... types) { + for (TbMsgType type : types) { + if (isTypeOf(type)) { + return true; + } + } + return false; + } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleEngineException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleEngineException.java index 1724a9050f..b8c713cc6b 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleEngineException.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleEngineException.java @@ -32,6 +32,11 @@ public class RuleEngineException extends Exception { ts = System.currentTimeMillis(); } + public RuleEngineException(String message, Throwable t) { + super(message != null ? message : "Unknown", t); + ts = System.currentTimeMillis(); + } + public String toJsonString() { try { return mapper.writeValueAsString(mapper.createObjectNode().put("message", getMessage())); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java index 3312c98b64..6cf298adb2 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java @@ -39,6 +39,10 @@ public interface TbMsgCallback { void onFailure(RuleEngineException e); + default void onRateLimit(RuleEngineException e) { + onFailure(e); + }; + /** * Returns 'true' if rule engine is expecting the message to be processed, 'false' otherwise. * message may no longer be valid, if the message pack is already expired/canceled/failed. diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java index dfcf178473..c1ca525cc9 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java @@ -15,6 +15,12 @@ */ package org.thingsboard.server.common.msg.session; +/** + * @deprecated This enum is deprecated and will be removed in a future version. + * Note: This enum was originally part of the public API but is now specific to CoAP transport only. + * Please use {@link org.thingsboard.server.transport.coap.CoapSessionMsgType} instead. + */ +@Deprecated(since="3.6.0", forRemoval = true) public enum SessionMsgType { GET_ATTRIBUTES_REQUEST(true), POST_ATTRIBUTES_REQUEST(true), GET_ATTRIBUTES_RESPONSE, SUBSCRIBE_ATTRIBUTES_REQUEST, UNSUBSCRIBE_ATTRIBUTES_REQUEST, ATTRIBUTES_UPDATE_NOTIFICATION, diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java index dd5fda4dd5..5e63cb037e 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java @@ -17,11 +17,12 @@ package org.thingsboard.server.common.msg.tools; import lombok.Getter; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.exception.AbstractRateLimitException; /** * Created by ashvayka on 22.10.18. */ -public class TbRateLimitsException extends RuntimeException { +public class TbRateLimitsException extends AbstractRateLimitException { @Getter private final EntityType entityType; diff --git a/common/pom.xml b/common/pom.xml index 51ca9546b1..0edd153a3c 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT thingsboard common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index a995d9b4a3..edd4574bdd 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/RuleEngineTbQueueAdminFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/RuleEngineTbQueueAdminFactory.java index fe29c3a04f..7a8764325f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/RuleEngineTbQueueAdminFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/RuleEngineTbQueueAdminFactory.java @@ -99,7 +99,7 @@ public class RuleEngineTbQueueAdminFactory { return new TbQueueAdmin() { @Override - public void createTopicIfNotExists(String topic) { + public void createTopicIfNotExists(String topic, String properties) { } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusAdmin.java index e171bb7a31..d95d2064a4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusAdmin.java @@ -22,6 +22,7 @@ import com.microsoft.azure.servicebus.primitives.MessagingEntityAlreadyExistsExc import com.microsoft.azure.servicebus.primitives.ServiceBusException; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.queue.TbQueueAdmin; +import org.thingsboard.server.queue.util.PropertyUtils; import java.io.IOException; import java.time.Duration; @@ -60,7 +61,7 @@ public class TbServiceBusAdmin implements TbQueueAdmin { } @Override - public void createTopicIfNotExists(String topic) { + public void createTopicIfNotExists(String topic, String properties) { if (queues.contains(topic)) { return; } @@ -68,7 +69,7 @@ public class TbServiceBusAdmin implements TbQueueAdmin { try { QueueDescription queueDescription = new QueueDescription(topic); queueDescription.setRequiresDuplicateDetection(false); - setQueueConfigs(queueDescription); + setQueueConfigs(queueDescription, PropertyUtils.getProps(queueConfigs, properties)); client.createQueue(queueDescription); queues.add(topic); @@ -107,7 +108,7 @@ public class TbServiceBusAdmin implements TbQueueAdmin { } } - private void setQueueConfigs(QueueDescription queueDescription) { + private void setQueueConfigs(QueueDescription queueDescription, Map queueConfigs) { queueConfigs.forEach((confKey, confValue) -> { switch (confKey) { case MAX_SIZE: diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java index 86146dabd1..2ebe41850d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java @@ -44,6 +44,7 @@ public abstract class AbstractTbQueueConsumerTemplate i protected volatile Set partitions; protected final ReentrantLock consumerLock = new ReentrantLock(); //NonfairSync final Queue> subscribeQueue = new ConcurrentLinkedQueue<>(); + protected volatile boolean queueDeleted = false; @Getter private final String topic; @@ -94,7 +95,7 @@ public abstract class AbstractTbQueueConsumerTemplate i partitions = subscribeQueue.poll(); } if (!subscribed) { - List topicNames = partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); + List topicNames = getFullTopicNames(); doSubscribe(topicNames); subscribed = true; } @@ -103,7 +104,9 @@ public abstract class AbstractTbQueueConsumerTemplate i consumerLock.unlock(); } - if (records.isEmpty()) { return sleepAndReturnEmpty(startNanos, durationInMillis); } + if (records.isEmpty() && !isLongPollingSupported()) { + return sleepAndReturnEmpty(startNanos, durationInMillis); + } return decodeRecords(records); } @@ -162,7 +165,9 @@ public abstract class AbstractTbQueueConsumerTemplate i @Override public void unsubscribe() { - log.info("unsubscribe topic and stop consumer {}", getTopic()); + log.info("Unsubscribing from topics and stopping consumer for topics {}", partitions.stream() + .map(TopicPartitionInfo::getFullTopicName) + .collect(Collectors.joining(", "))); stopped = true; consumerLock.lock(); try { @@ -187,4 +192,22 @@ public abstract class AbstractTbQueueConsumerTemplate i abstract protected void doUnsubscribe(); + @Override + public void onQueueDelete() { + queueDeleted = true; + } + + public boolean isQueueDeleted() { + return queueDeleted; + } + + @Override + public List getFullTopicNames() { + return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); + } + + protected boolean isLongPollingSupported() { + return false; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueCallbackWrapper.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueCallbackWrapper.java index f14c9a047b..f102e34666 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueCallbackWrapper.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueCallbackWrapper.java @@ -40,6 +40,6 @@ public class MultipleTbQueueCallbackWrapper implements TbQueueCallback { @Override public void onFailure(Throwable t) { - callback.onFailure(new RuleEngineException(t.getMessage())); + callback.onFailure(new RuleEngineException(t.getMessage(), t)); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueTbMsgCallbackWrapper.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueTbMsgCallbackWrapper.java index e812628f16..b3bd05ca5e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueTbMsgCallbackWrapper.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueTbMsgCallbackWrapper.java @@ -41,6 +41,6 @@ public class MultipleTbQueueTbMsgCallbackWrapper implements TbQueueCallback { @Override public void onFailure(Throwable t) { - tbMsgCallback.onFailure(new RuleEngineException(t.getMessage())); + tbMsgCallback.onFailure(new RuleEngineException(t.getMessage(), t)); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbQueueTbMsgCallbackWrapper.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbQueueTbMsgCallbackWrapper.java index 413ef11e30..1107115085 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbQueueTbMsgCallbackWrapper.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbQueueTbMsgCallbackWrapper.java @@ -35,6 +35,6 @@ public class TbQueueTbMsgCallbackWrapper implements TbQueueCallback { @Override public void onFailure(Throwable t) { - tbMsgCallback.onFailure(new RuleEngineException(t.getMessage())); + tbMsgCallback.onFailure(new RuleEngineException(t.getMessage(), t)); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java index 3c85aef350..64a8b70a1f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java @@ -23,6 +23,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbTransportService; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; @@ -35,6 +36,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import static org.thingsboard.common.util.SystemUtil.getCpuCount; @@ -57,6 +60,10 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { @Value("${service.type:monolith}") private String serviceType; + @Getter + @Value("${service.rule_engine.assigned_tenant_profiles:}") + private Set assignedTenantProfiles; + @Autowired private ApplicationContext applicationContext; @@ -78,6 +85,9 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { } else { serviceTypes = Collections.singletonList(ServiceType.of(serviceType)); } + if (!serviceTypes.contains(ServiceType.TB_RULE_ENGINE) || assignedTenantProfiles == null) { + assignedTenantProfiles = Collections.emptySet(); + } generateNewServiceInfoWithCurrentSystemInfo(); } @@ -111,7 +121,9 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { .setServiceId(serviceId) .addAllServiceTypes(serviceTypes.stream().map(ServiceType::name).collect(Collectors.toList())) .setSystemInfo(getCurrentSystemInfoProto()); - + if (CollectionsUtil.isNotEmpty(assignedTenantProfiles)) { + builder.addAllAssignedTenantProfiles(assignedTenantProfiles.stream().map(UUID::toString).collect(Collectors.toList())); + } return serviceInfo = builder.build(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 14fb0369ee..2db342d4e6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -24,6 +24,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.exception.TenantNotFoundException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos; @@ -36,6 +37,7 @@ import org.thingsboard.server.queue.util.AfterStartUp; import javax.annotation.PostConstruct; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -48,6 +50,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; + @Service @Slf4j public class HashPartitionService implements PartitionService { @@ -68,15 +72,16 @@ public class HashPartitionService implements PartitionService { private final TenantRoutingInfoService tenantRoutingInfoService; private final QueueRoutingInfoService queueRoutingInfoService; - private volatile ConcurrentMap> myPartitions = new ConcurrentHashMap<>(); + protected volatile ConcurrentMap> myPartitions = new ConcurrentHashMap<>(); private final ConcurrentMap partitionTopicsMap = new ConcurrentHashMap<>(); private final ConcurrentMap partitionSizesMap = new ConcurrentHashMap<>(); private final ConcurrentMap tenantRoutingInfoMap = new ConcurrentHashMap<>(); - private Map> tbTransportServicesByType = new HashMap<>(); private List currentOtherServices; + private final Map> tbTransportServicesByType = new HashMap<>(); + private final Map> responsibleServices = new HashMap<>(); private HashFunction hashFunction; @@ -165,6 +170,9 @@ public class HashPartitionService implements PartitionService { partitionTopicsMap.put(queueKey, queueUpdateMsg.getQueueTopic()); partitionSizesMap.put(queueKey, queueUpdateMsg.getPartitions()); myPartitions.remove(queueKey); + if (!tenantId.isSysTenantId()) { + tenantRoutingInfoMap.remove(tenantId); + } } @Override @@ -178,12 +186,38 @@ public class HashPartitionService implements PartitionService { removeTenant(tenantId); } + @Override + public boolean isManagedByCurrentService(TenantId tenantId) { + Set assignedTenantProfiles = serviceInfoProvider.getAssignedTenantProfiles(); + if (assignedTenantProfiles.isEmpty()) { + // TODO: refactor this for common servers + return true; + } else { + if (tenantId.isSysTenantId()) { + return false; + } + TenantProfileId profileId = tenantRoutingInfoService.getRoutingInfo(tenantId).getProfileId(); + return assignedTenantProfiles.contains(profileId.getId()); + } + } + @Override public TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId) { TenantId isolatedOrSystemTenantId = getIsolatedOrSystemTenantId(serviceType, tenantId); + if (queueName == null) { + queueName = MAIN_QUEUE_NAME; + } QueueKey queueKey = new QueueKey(serviceType, queueName, isolatedOrSystemTenantId); if (!partitionSizesMap.containsKey(queueKey)) { - queueKey = new QueueKey(serviceType, isolatedOrSystemTenantId); + if (isolatedOrSystemTenantId.isSysTenantId()) { + queueKey = new QueueKey(serviceType, TenantId.SYS_TENANT_ID); + } else { + queueKey = new QueueKey(serviceType, queueName, TenantId.SYS_TENANT_ID); + if (!MAIN_QUEUE_NAME.equals(queueName) && !partitionSizesMap.containsKey(queueKey)) { + queueKey = new QueueKey(serviceType, TenantId.SYS_TENANT_ID); + } + log.warn("Using queue {} instead of isolated {} for tenant {}", queueKey, queueName, isolatedOrSystemTenantId); + } } return resolve(queueKey, entityId); } @@ -199,11 +233,12 @@ public class HashPartitionService implements PartitionService { } private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) { - int hash = hashFunction.newHasher() - .putLong(entityId.getId().getMostSignificantBits()) - .putLong(entityId.getId().getLeastSignificantBits()).hash().asInt(); - Integer partitionSize = partitionSizesMap.get(queueKey); + if (partitionSize == null) { + throw new IllegalStateException("Partitions info for queue " + queueKey + " is missing"); + } + + int hash = hash(entityId.getId()); int partition = Math.abs(hash % partitionSize); return buildTopicPartitionInfo(queueKey, partition); @@ -212,6 +247,7 @@ public class HashPartitionService implements PartitionService { @Override public synchronized void recalculatePartitions(ServiceInfo currentService, List otherServices) { tbTransportServicesByType.clear(); + responsibleServices.clear(); logServiceInfo(currentService); otherServices.forEach(this::logServiceInfo); @@ -221,6 +257,7 @@ public class HashPartitionService implements PartitionService { addNode(queueServicesMap, other); } queueServicesMap.values().forEach(list -> list.sort(Comparator.comparing(ServiceInfo::getServiceId))); + responsibleServices.values().forEach(list -> list.sort(Comparator.comparing(ServiceInfo::getServiceId))); final ConcurrentMap> newPartitions = new ConcurrentHashMap<>(); partitionSizesMap.forEach((queueKey, size) -> { @@ -268,6 +305,9 @@ public class HashPartitionService implements PartitionService { changes.addAll(newMap.keySet()); if (!changes.isEmpty()) { applicationEventPublisher.publishEvent(new ClusterTopologyChangeEvent(this, changes)); + responsibleServices.forEach((profileId, serviceInfos) -> { + log.info("Servers responsible for tenant profile {}: {}", profileId, toServiceIds(serviceInfos)); + }); } } @@ -305,9 +345,7 @@ public class HashPartitionService implements PartitionService { @Override public int resolvePartitionIndex(UUID entityId, int partitions) { - int hash = hashFunction.newHasher() - .putLong(entityId.getMostSignificantBits()) - .putLong(entityId.getLeastSignificantBits()).hash().asInt(); + int hash = hash(entityId); return Math.abs(hash % partitions); } @@ -358,16 +396,9 @@ public class HashPartitionService implements PartitionService { if (TenantId.SYS_TENANT_ID.equals(tenantId)) { return false; } - TenantRoutingInfo routingInfo = tenantRoutingInfoMap.get(tenantId); - if (routingInfo == null) { - synchronized (tenantRoutingInfoMap) { - routingInfo = tenantRoutingInfoMap.get(tenantId); - if (routingInfo == null) { - routingInfo = tenantRoutingInfoService.getRoutingInfo(tenantId); - tenantRoutingInfoMap.put(tenantId, routingInfo); - } - } - } + TenantRoutingInfo routingInfo = tenantRoutingInfoMap.computeIfAbsent(tenantId, k -> { + return tenantRoutingInfoService.getRoutingInfo(tenantId); + }); if (routingInfo == null) { throw new TenantNotFoundException(tenantId); } @@ -396,6 +427,19 @@ public class HashPartitionService implements PartitionService { queueServiceList.computeIfAbsent(key, k -> new ArrayList<>()).add(instance); } }); + + if (instance.getAssignedTenantProfilesCount() > 0) { + for (String profileIdStr : instance.getAssignedTenantProfilesList()) { + TenantProfileId profileId; + try { + profileId = new TenantProfileId(UUID.fromString(profileIdStr)); + } catch (IllegalArgumentException e) { + log.warn("Failed to parse '{}' as tenant profile id", profileIdStr); + continue; + } + responsibleServices.computeIfAbsent(profileId, k -> new ArrayList<>()).add(instance); + } + } } else if (ServiceType.TB_CORE.equals(serviceType) || ServiceType.TB_VC_EXECUTOR.equals(serviceType)) { queueServiceList.computeIfAbsent(new QueueKey(serviceType), key -> new ArrayList<>()).add(instance); } @@ -411,18 +455,51 @@ public class HashPartitionService implements PartitionService { return null; } - if (!ServiceType.TB_RULE_ENGINE.equals(queueKey.getType()) || TenantId.SYS_TENANT_ID.equals(queueKey.getTenantId())) { - return servers.get(partition % servers.size()); - } else { - int hash = hashFunction.newHasher().putLong(queueKey.getTenantId().getId().getMostSignificantBits()) - .putLong(queueKey.getTenantId().getId().getLeastSignificantBits()) + TenantId tenantId = queueKey.getTenantId(); + if (queueKey.getType() == ServiceType.TB_RULE_ENGINE) { + if (!responsibleServices.isEmpty()) { // if there are any dedicated servers + TenantProfileId profileId; + if (tenantId != null && !tenantId.isSysTenantId()) { + TenantRoutingInfo routingInfo = tenantRoutingInfoService.getRoutingInfo(tenantId); + profileId = routingInfo.getProfileId(); + } else { + profileId = null; + } + + List responsible = responsibleServices.get(profileId); + if (responsible == null) { + // if there are no dedicated servers for this tenant profile, or for system queues, + // using the servers that are not responsible for any profile + responsible = servers.stream() + .filter(serviceInfo -> serviceInfo.getAssignedTenantProfilesCount() == 0) + .sorted(Comparator.comparing(ServiceInfo::getServiceId)) + .collect(Collectors.toList()); + if (profileId != null) { + log.debug("Using servers {} for profile {}", toServiceIds(responsible), profileId); + } + responsibleServices.put(profileId, responsible); + } + servers = responsible; + } + + int hash = hashFunction.newHasher() + .putLong(tenantId.getId().getMostSignificantBits()) + .putLong(tenantId.getId().getLeastSignificantBits()) .putString(queueKey.getQueueName(), StandardCharsets.UTF_8) .hash().asInt(); - return servers.get(Math.abs((hash + partition) % servers.size())); + } else { + return servers.get(partition % servers.size()); } } + private int hash(UUID key) { + return hashFunction.newHasher() + .putLong(key.getMostSignificantBits()) + .putLong(key.getLeastSignificantBits()) + .hash().asInt(); + } + public static HashFunction forName(String name) { switch (name) { case "murmur3_32": @@ -436,4 +513,8 @@ public class HashPartitionService implements PartitionService { } } + private List toServiceIds(Collection serviceInfos) { + return serviceInfos.stream().map(ServiceInfo::getServiceId).collect(Collectors.toList()); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java index faa4d956a8..b55ba79f67 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java @@ -64,4 +64,7 @@ public interface PartitionService { void updateQueue(TransportProtos.QueueUpdateMsg queueUpdateMsg); void removeQueue(TransportProtos.QueueDeleteMsg queueDeleteMsg); + + boolean isManagedByCurrentService(TenantId tenantId); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java index e49cbbcfd9..9c7d1630ec 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java @@ -18,6 +18,9 @@ package org.thingsboard.server.queue.discovery; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; +import java.util.Set; +import java.util.UUID; + public interface TbServiceInfoProvider { String getServiceId(); @@ -30,4 +33,6 @@ public interface TbServiceInfoProvider { ServiceInfo generateNewServiceInfoWithCurrentSystemInfo(); + Set getAssignedTenantProfiles(); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TenantRoutingInfo.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TenantRoutingInfo.java index c1c0b49dab..8dee68da49 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TenantRoutingInfo.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TenantRoutingInfo.java @@ -17,9 +17,11 @@ package org.thingsboard.server.queue.discovery; import lombok.Data; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; @Data public class TenantRoutingInfo { private final TenantId tenantId; + private final TenantProfileId profileId; private final boolean isolatedTbRuleEngine; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TenantRoutingInfoService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TenantRoutingInfoService.java index 8dd3ff95e7..e4c0ac8250 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TenantRoutingInfoService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TenantRoutingInfoService.java @@ -20,4 +20,5 @@ import org.thingsboard.server.common.data.id.TenantId; public interface TenantRoutingInfoService { TenantRoutingInfo getRoutingInfo(TenantId tenantId); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java index fcf80bcf3d..e99817de17 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.queue.discovery; import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.ProtocolStringList; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.curator.framework.CuratorFramework; @@ -44,8 +45,10 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.List; import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -66,6 +69,10 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi private Integer zkSessionTimeout; @Value("${zk.zk_dir}") private String zkDir; + @Value("${zk.recalculate_delay:0}") + private Long recalculateDelay; + + protected final ConcurrentHashMap> delayedTasks; private final TbServiceInfoProvider serviceInfoProvider; private final PartitionService partitionService; @@ -82,6 +89,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi PartitionService partitionService) { this.serviceInfoProvider = serviceInfoProvider; this.partitionService = partitionService; + delayedTasks = new ConcurrentHashMap<>(); } @PostConstruct @@ -287,11 +295,39 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi log.error("Failed to decode server instance for node {}", data.getPath(), e); throw e; } - log.debug("Processing [{}] event for [{}]", pathChildrenCacheEvent.getType(), instance.getServiceId()); + + String serviceId = instance.getServiceId(); + ProtocolStringList serviceTypesList = instance.getServiceTypesList(); + + log.trace("Processing [{}] event for [{}]", pathChildrenCacheEvent.getType(), serviceId); switch (pathChildrenCacheEvent.getType()) { case CHILD_ADDED: + ScheduledFuture task = delayedTasks.remove(serviceId); + if (task != null) { + if (task.cancel(false)) { + log.debug("[{}] Recalculate partitions ignored. Service was restarted in time [{}].", + serviceId, serviceTypesList); + } else { + log.debug("[{}] Going to recalculate partitions. Service was not restarted in time [{}]!", + serviceId, serviceTypesList); + recalculatePartitions(); + } + } else { + log.trace("[{}] Going to recalculate partitions due to adding new node [{}].", + serviceId, serviceTypesList); + recalculatePartitions(); + } + break; case CHILD_REMOVED: - recalculatePartitions(); + ScheduledFuture future = zkExecutorService.schedule(() -> { + log.debug("[{}] Going to recalculate partitions due to removed node [{}]", + serviceId, serviceTypesList); + ScheduledFuture removedTask = delayedTasks.remove(serviceId); + if (removedTask != null) { + recalculatePartitions(); + } + }, recalculateDelay, TimeUnit.MILLISECONDS); + delayedTasks.put(serviceId, future); break; default: break; @@ -303,6 +339,8 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi * Synchronized to ensure that other servers info is up to date * */ synchronized void recalculatePartitions() { + delayedTasks.values().forEach(future -> future.cancel(false)); + delayedTasks.clear(); partitionService.recalculatePartitions(serviceInfoProvider.getServiceInfo(), getOtherServers()); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java index f15b9258e8..d486d04783 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java @@ -21,6 +21,7 @@ import org.apache.kafka.clients.admin.CreateTopicsResult; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.common.errors.TopicExistsException; import org.thingsboard.server.queue.TbQueueAdmin; +import org.thingsboard.server.queue.util.PropertyUtils; import java.util.Collections; import java.util.Map; @@ -62,12 +63,12 @@ public class TbKafkaAdmin implements TbQueueAdmin { } @Override - public void createTopicIfNotExists(String topic) { + public void createTopicIfNotExists(String topic, String properties) { if (topics.contains(topic)) { return; } try { - NewTopic newTopic = new NewTopic(topic, numPartitions, replicationFactor).configs(topicConfigs); + NewTopic newTopic = new NewTopic(topic, numPartitions, replicationFactor).configs(PropertyUtils.getProps(topicConfigs, properties)); createTopic(newTopic).values().get(topic).get(); topics.add(topic); } catch (ExecutionException ee) { @@ -81,7 +82,6 @@ public class TbKafkaAdmin implements TbQueueAdmin { log.warn("[{}] Failed to create topic", topic, e); throw new RuntimeException(e); } - } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java index c17a563d46..9f58446966 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java @@ -114,7 +114,6 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue @Override protected void doUnsubscribe() { - log.info("unsubscribe topic and close consumer for topic {}", getTopic()); if (consumer != null) { consumer.unsubscribe(); consumer.close(); @@ -123,4 +122,10 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue statsService.unregisterClientGroup(groupId); } } + + @Override + public boolean isLongPollingSupported() { + return true; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java index 15c2f04d17..850f5c08ea 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java @@ -30,6 +30,9 @@ import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.TbQueueProducer; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -53,10 +56,14 @@ public class TbKafkaProducerTemplate implements TbQueuePro private final Set topics; + @Getter + private final String clientId; + @Builder private TbKafkaProducerTemplate(TbKafkaSettings settings, String defaultTopic, String clientId, TbQueueAdmin admin) { Properties props = settings.toProducerProps(); + this.clientId = Objects.requireNonNull(clientId, "Kafka producer client.id is null"); if (!StringUtils.isEmpty(clientId)) { props.put(ProducerConfig.CLIENT_ID_CONFIG, clientId); } @@ -72,6 +79,22 @@ public class TbKafkaProducerTemplate implements TbQueuePro public void init() { } + void addAnalyticHeaders(List
headers) { + headers.add(new RecordHeader("_producerId", getClientId().getBytes(StandardCharsets.UTF_8))); + headers.add(new RecordHeader("_threadName", Thread.currentThread().getName().getBytes(StandardCharsets.UTF_8))); + if (log.isTraceEnabled()) { + try { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + int maxlevel = Math.min(stackTrace.length, 10); + for (int i = 2; i < maxlevel; i++) { // ignore two levels: getStackTrace and addAnalyticHeaders + headers.add(new RecordHeader("_stackTrace" + i, stackTrace[i].toString().getBytes(StandardCharsets.UTF_8))); + } + } catch (Throwable t) { + log.trace("Failed to add stacktrace headers in Kafka producer {}", getClientId(), t); + } + } + } + @Override public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) { try { @@ -79,7 +102,10 @@ public class TbKafkaProducerTemplate implements TbQueuePro String key = msg.getKey().toString(); byte[] data = msg.getData(); ProducerRecord record; - Iterable
headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList()); + List
headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList()); + if (log.isDebugEnabled()) { + addAnalyticHeaders(headers); + } record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers); producer.send(record, (metadata, exception) -> { if (exception == null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index 41ae0d5ea3..55f1721c46 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -115,6 +115,9 @@ public class TbKafkaSettings { @Value("${queue.kafka.session.timeout.ms:10000}") private int sessionTimeoutMs; + @Value("${queue.kafka.auto_offset_reset:earliest}") + private String autoOffsetReset; + @Value("${queue.kafka.use_confluent_cloud:false}") private boolean useConfluent; @@ -155,6 +158,8 @@ public class TbKafkaSettings { props.put(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, maxPartitionFetchBytes); props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes); props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalMs); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java index 081202315e..8711cbbcf1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java @@ -31,6 +31,7 @@ public class InMemoryTbQueueConsumer implements TbQueueCon private volatile Set partitions; private volatile boolean stopped; private volatile boolean subscribed; + private volatile boolean queueDeleted; public InMemoryTbQueueConsumer(InMemoryStorage storage, String topic) { this.storage = storage; @@ -103,4 +104,19 @@ public class InMemoryTbQueueConsumer implements TbQueueCon return stopped; } + @Override + public void onQueueDelete() { + queueDeleted = true; + } + + @Override + public boolean isQueueDeleted() { + return queueDeleted; + } + + @Override + public List getFullTopicNames() { + return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java index 6b6253acc4..a789816c34 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java @@ -74,7 +74,7 @@ public class InMemoryTbTransportQueueFactory implements TbTransportQueueFactory templateBuilder.queueAdmin(new TbQueueAdmin() { @Override - public void createTopicIfNotExists(String topic) {} + public void createTopicIfNotExists(String topic, String properties) {} @Override public void destroy() {} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 364abcff4c..22a16de64f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -187,7 +187,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(configuration.getTopic()); consumerBuilder.clientId("re-" + queueName + "-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); - consumerBuilder.groupId("re-" + queueName + "-consumer"); + consumerBuilder.groupId("re-" + queueName + (configuration.getTenantId().isSysTenantId() ? "" : ("-" + configuration.getTenantId())) + "-consumer"); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(ruleEngineAdmin); consumerBuilder.statsService(consumerStatsService); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index b8e07a45f7..2e3bf784d7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -166,7 +166,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(configuration.getTopic()); consumerBuilder.clientId("re-" + queueName + "-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); - consumerBuilder.groupId("re-" + queueName + "-consumer"); + consumerBuilder.groupId("re-" + queueName + (configuration.getTenantId().isSysTenantId() ? "" : ("-" + configuration.getTenantId())) + "-consumer"); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(ruleEngineAdmin); consumerBuilder.statsService(consumerStatsService); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubAdmin.java index d1a4942ad3..f9f20c2448 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubAdmin.java @@ -103,7 +103,7 @@ public class TbPubSubAdmin implements TbQueueAdmin { } @Override - public void createTopicIfNotExists(String partition) { + public void createTopicIfNotExists(String partition, String properties) { TopicName topicName = TopicName.newBuilder() .setTopic(partition) .setProject(pubSubSettings.getProjectId()) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqAdmin.java index 00a2ee4c6c..fb646f383a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqAdmin.java @@ -18,9 +18,11 @@ package org.thingsboard.server.queue.rabbitmq; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.queue.TbQueueAdmin; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeoutException; @@ -50,7 +52,12 @@ public class TbRabbitMqAdmin implements TbQueueAdmin { } @Override - public void createTopicIfNotExists(String topic) { + public void createTopicIfNotExists(String topic, String properties) { + Map arguments = this.arguments; + if (StringUtils.isNotBlank(properties)) { + arguments = new HashMap<>(arguments); + arguments.putAll(TbRabbitMqQueueArguments.getArgs(properties)); + } try { channel.queueDeclare(topic, false, false, false, arguments); } catch (IOException e) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqQueueArguments.java b/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqQueueArguments.java index cb96abdf3c..8fa8c537e6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqQueueArguments.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqQueueArguments.java @@ -65,7 +65,7 @@ public class TbRabbitMqQueueArguments { vcArgs = getArgs(vcProperties); } - private Map getArgs(String properties) { + public static Map getArgs(String properties) { Map configs = new HashMap<>(); if (StringUtils.isNotEmpty(properties)) { for (String property : properties.split(";")) { @@ -78,7 +78,7 @@ public class TbRabbitMqQueueArguments { return configs; } - private Object getObjectValue(String str) { + private static Object getObjectValue(String str) { if (str.equalsIgnoreCase("true") || str.equalsIgnoreCase("false")) { return Boolean.valueOf(str); } else if (isNumeric(str)) { @@ -87,7 +87,7 @@ public class TbRabbitMqQueueArguments { return str; } - private Object getNumericValue(String str) { + private static Object getNumericValue(String str) { if (str.contains(".")) { return Double.valueOf(str); } else { @@ -97,7 +97,7 @@ public class TbRabbitMqQueueArguments { private static final Pattern PATTERN = Pattern.compile("-?\\d+(\\.\\d+)?"); - public boolean isNumeric(String strNum) { + private static boolean isNumeric(String strNum) { if (strNum == null) { return false; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsAdmin.java index f88a34941a..ba4eeb6ca4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsAdmin.java @@ -26,6 +26,7 @@ import com.amazonaws.services.sqs.model.CreateQueueRequest; import com.amazonaws.services.sqs.model.GetQueueUrlResult; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.queue.TbQueueAdmin; +import org.thingsboard.server.queue.util.PropertyUtils; import java.util.Map; import java.util.function.Function; @@ -63,11 +64,12 @@ public class TbAwsSqsAdmin implements TbQueueAdmin { } @Override - public void createTopicIfNotExists(String topic) { + public void createTopicIfNotExists(String topic, String properties) { String queueName = convertTopicToQueueName(topic); if (queues.containsKey(queueName)) { return; } + Map attributes = PropertyUtils.getProps(this.attributes, properties, TbAwsSqsQueueAttributes::toConfigs); final CreateQueueRequest createQueueRequest = new CreateQueueRequest(queueName).withAttributes(attributes); String queueUrl = sqsClient.createQueue(createQueueRequest).getQueueUrl(); queues.put(getQueueNameFromUrl(queueUrl), queueUrl); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java b/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java index 66110ade74..faa8eccc90 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java @@ -76,6 +76,12 @@ public class TbAwsSqsQueueAttributes { private Map getConfigs(String properties) { Map configs = new HashMap<>(defaultAttributes); + configs.putAll(toConfigs(properties)); + return configs; + } + + public static Map toConfigs(String properties) { + Map configs = new HashMap<>(); if (StringUtils.isNotEmpty(properties)) { for (String property : properties.split(";")) { int delimiterPosition = property.indexOf(":"); @@ -88,7 +94,7 @@ public class TbAwsSqsQueueAttributes { return configs; } - private void validateAttributeName(String key) { + private static void validateAttributeName(String key) { QueueAttributeName.fromValue(key); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.java index 089d7f2219..afee64f382 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.java @@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.StringUtils; import java.util.HashMap; import java.util.Map; +import java.util.function.Function; public class PropertyUtils { @@ -37,4 +38,17 @@ public class PropertyUtils { return configs; } + public static Map getProps(Map defaultProperties, String propertiesStr) { + return getProps(defaultProperties, propertiesStr, PropertyUtils::getProps); + } + + public static Map getProps(Map defaultProperties, String propertiesStr, Function> parser) { + Map properties = defaultProperties; + if (StringUtils.isNotBlank(propertiesStr)) { + properties = new HashMap<>(properties); + properties.putAll(parser.apply(propertiesStr)); + } + return properties; + } + } diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java new file mode 100644 index 0000000000..a8810efd0e --- /dev/null +++ b/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java @@ -0,0 +1,189 @@ +/** + * Copyright © 2016-2023 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.queue.discovery; + +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.imps.CuratorFrameworkState; +import org.apache.curator.framework.recipes.cache.ChildData; +import org.apache.curator.framework.recipes.cache.PathChildrenCache; +import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import static org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent.Type.CHILD_ADDED; +import static org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent.Type.CHILD_REMOVED; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ZkDiscoveryServiceTest { + + @Mock + private TbServiceInfoProvider serviceInfoProvider; + + @Mock + private PartitionService partitionService; + + @Mock + private CuratorFramework client; + + @Mock + private PathChildrenCache cache; + + @Mock + private CuratorFramework curatorFramework; + + private ZkDiscoveryService zkDiscoveryService; + + private static final long RECALCULATE_DELAY = 100L; + + final TransportProtos.ServiceInfo currentInfo = TransportProtos.ServiceInfo.newBuilder().setServiceId("tb-rule-engine-0").build(); + final ChildData currentData = new ChildData("/thingsboard/nodes/0000000010", null, currentInfo.toByteArray()); + final TransportProtos.ServiceInfo childInfo = TransportProtos.ServiceInfo.newBuilder().setServiceId("tb-rule-engine-1").build(); + final ChildData childData = new ChildData("/thingsboard/nodes/0000000020", null, childInfo.toByteArray()); + + @Before + public void setup() { + zkDiscoveryService = Mockito.spy(new ZkDiscoveryService(serviceInfoProvider, partitionService)); + ScheduledExecutorService zkExecutorService = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("zk-discovery")); + when(client.getState()).thenReturn(CuratorFrameworkState.STARTED); + ReflectionTestUtils.setField(zkDiscoveryService, "stopped", false); + ReflectionTestUtils.setField(zkDiscoveryService, "client", client); + ReflectionTestUtils.setField(zkDiscoveryService, "cache", cache); + ReflectionTestUtils.setField(zkDiscoveryService, "nodePath", "/thingsboard/nodes/0000000010"); + ReflectionTestUtils.setField(zkDiscoveryService, "zkExecutorService", zkExecutorService); + ReflectionTestUtils.setField(zkDiscoveryService, "recalculateDelay", RECALCULATE_DELAY); + ReflectionTestUtils.setField(zkDiscoveryService, "zkDir", "/thingsboard"); + + when(serviceInfoProvider.getServiceInfo()).thenReturn(currentInfo); + + List dataList = new ArrayList<>(); + dataList.add(currentData); + when(cache.getCurrentData()).thenReturn(dataList); + } + + @Test + public void restartNodeInTimeTest() throws Exception { + startNode(childData); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(childInfo))); + + reset(partitionService); + + stopNode(childData); + + assertEquals(1, zkDiscoveryService.delayedTasks.size()); + + verify(partitionService, never()).recalculatePartitions(any(), any()); + + startNode(childData); + + verify(partitionService, never()).recalculatePartitions(any(), any()); + + Thread.sleep(RECALCULATE_DELAY * 2); + + verify(partitionService, never()).recalculatePartitions(any(), any()); + + assertTrue(zkDiscoveryService.delayedTasks.isEmpty()); + } + + @Test + public void restartNodeNotInTimeTest() throws Exception { + startNode(childData); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(childInfo))); + + reset(partitionService); + + stopNode(childData); + + assertEquals(1, zkDiscoveryService.delayedTasks.size()); + + Thread.sleep(RECALCULATE_DELAY * 2); + + assertTrue(zkDiscoveryService.delayedTasks.isEmpty()); + + startNode(childData); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(Collections.emptyList())); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(childInfo))); + + reset(partitionService); + } + + @Test + public void startAnotherNodeDuringRestartTest() throws Exception { + var anotherInfo = TransportProtos.ServiceInfo.newBuilder().setServiceId("tb-transport").build(); + var anotherData = new ChildData("/thingsboard/nodes/0000000030", null, anotherInfo.toByteArray()); + + startNode(childData); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(childInfo))); + + reset(partitionService); + + stopNode(childData); + + assertEquals(1, zkDiscoveryService.delayedTasks.size()); + + startNode(anotherData); + + assertTrue(zkDiscoveryService.delayedTasks.isEmpty()); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(anotherInfo))); + reset(partitionService); + + Thread.sleep(RECALCULATE_DELAY * 2); + + verify(partitionService, never()).recalculatePartitions(any(), any()); + + startNode(childData); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(anotherInfo, childInfo))); + } + + private void startNode(ChildData data) throws Exception { + cache.getCurrentData().add(data); + zkDiscoveryService.childEvent(curatorFramework, new PathChildrenCacheEvent(CHILD_ADDED, data)); + } + + private void stopNode(ChildData data) throws Exception { + cache.getCurrentData().remove(data); + zkDiscoveryService.childEvent(curatorFramework, new PathChildrenCacheEvent(CHILD_REMOVED, data)); + } + +} diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplateTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplateTest.java new file mode 100644 index 0000000000..bfd3c4a6dc --- /dev/null +++ b/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplateTest.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2023 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.queue.kafka; + +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.common.header.Header; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.queue.TbQueueMsg; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willCallRealMethod; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; + +@Slf4j +class TbKafkaProducerTemplateTest { + + TbKafkaProducerTemplate producerTemplate; + + @BeforeEach + void setUp() { + producerTemplate = mock(TbKafkaProducerTemplate.class); + willCallRealMethod().given(producerTemplate).addAnalyticHeaders(any()); + willReturn("tb-core-to-core-notifications-tb-core-3").given(producerTemplate).getClientId(); + } + + @Test + void testAddAnalyticHeaders() { + List
headers = new ArrayList<>(); + producerTemplate.addAnalyticHeaders(headers); + assertThat(headers).isNotEmpty(); + headers.forEach(r -> log.info("RecordHeader key [{}] value [{}]", r.key(), new String(r.value(), StandardCharsets.UTF_8))); + } + +} diff --git a/common/queue/src/test/resources/logback-test.xml b/common/queue/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..f7053313d4 --- /dev/null +++ b/common/queue/src/test/resources/logback-test.xml @@ -0,0 +1,20 @@ + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/common/script/pom.xml b/common/script/pom.xml index 7a4304b77a..26f60bd358 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index 53e222ad76..8fa852d977 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index ae5f38f42f..c4e069f695 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java index bbf441a659..2a60980f84 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java @@ -66,8 +66,6 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem protected final Map scriptIdToHash = new ConcurrentHashMap<>(); protected final Map scriptMap = new ConcurrentHashMap<>(); - private final String tbelSwitch = "switch"; - private final String tbelSwitchErrorMsg = "TBEL does not support the 'switch'."; protected Cache compiledScriptsCache; private SandboxedParserConfiguration parserConfig; @@ -183,11 +181,6 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem lock.unlock(); } return scriptId; - } catch (CompileException ce) { - if ( ce.getExpr() != null && new String(ce.getExpr()).contains(tbelSwitch)) { - ce = new CompileException(tbelSwitchErrorMsg, ce.getExpr(), ce.getCursor(), ce.getCause()); - } - throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, ce); } catch (Exception e) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java index 9c360e5c7e..c22e2c329d 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java @@ -35,7 +35,8 @@ public class TbDate extends Date { private static final DateTimeFormatter isoDateFormatter = DateTimeFormatter.ofPattern( "yyyy-MM-dd[[ ]['T']HH:mm[:ss[.SSS]][ ][XXX][Z][z][VV][O]]").withZone(ZoneId.systemDefault()); - private static final DateFormat isoDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + private static final ThreadLocal isoDateFormat = ThreadLocal.withInitial(() -> + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")); public TbDate() { super(); @@ -73,7 +74,7 @@ public class TbDate extends Date { } public String toISOString() { - return isoDateFormat.format(this); + return isoDateFormat.get().format(this); } public String toLocaleString(String locale) { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index b337612011..bffd60f029 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -57,6 +57,8 @@ public class TbUtils { List.class))); parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson", ExecutionContext.class, List.class))); + parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson", + ExecutionContext.class, String.class))); parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes", ExecutionContext.class, String.class))); parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes", @@ -174,6 +176,9 @@ public class TbUtils { public static Object decodeToJson(ExecutionContext ctx, List bytesList) throws IOException { return TbJson.parse(ctx, bytesToString(bytesList)); } + public static Object decodeToJson(ExecutionContext ctx, String jsonStr) throws IOException { + return TbJson.parse(ctx, jsonStr); + } public static String bytesToString(List bytesList) { byte[] bytes = bytesFromList(bytesList); diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java new file mode 100644 index 0000000000..88f1f825c1 --- /dev/null +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java @@ -0,0 +1,127 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api.tbel; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +class TbDateTest { + + ListeningExecutorService executor; + + @AfterEach + void tearDown() { + if (executor != null) { + executor.shutdownNow(); + } + } + + /** + * Note: This test simulated the high concurrency calls. + * But it not always fails before as the concurrency issue happens far away from the test subject, inside the SimpleDateFormat class. + * Few calls later after latch open, the concurrency issue is not well reproduced. + * Depends on environment some failure may happen each 2 or 100 runs. + * To be highly confident run this test in a ForkMode=method, repeat 100 times. This will provide about 99 failures per 100 runs. + * The value of this test is *never* to fail when isoDateFormat.format(this) is properly synchronized + * If this test fails time-to-time -- it is a sign that isoDateFormat.format() called concurrently and have to be fixed (synchronized) + * The expected exception example: + * Caused by: java.lang.ArrayIndexOutOfBoundsException: Index 14 out of bounds for length 13 + * at java.base/sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:457) + * at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2394) + * at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2309) + * at java.base/java.util.Calendar.setTimeInMillis(Calendar.java:1834) + * at java.base/java.util.Calendar.setTime(Calendar.java:1800) + * at java.base/java.text.SimpleDateFormat.format(SimpleDateFormat.java:974) + */ + @Test + void testToISOStringConcurrently() throws ExecutionException, InterruptedException, TimeoutException { + int threads = 5; + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(threads)); + for (int j = 0; j < 1000; j++) { + final int iteration = j; + CountDownLatch readyLatch = new CountDownLatch(threads); + CountDownLatch latch = new CountDownLatch(1); + long now = 1709217342000L; + List> futures = new ArrayList<>(threads); + for (int i = 0; i < threads; i++) { + long ts = now + TimeUnit.DAYS.toMillis(i * 366) + TimeUnit.MINUTES.toMillis(iteration) + TimeUnit.SECONDS.toMillis(iteration) + iteration; + TbDate tbDate = new TbDate(ts); + futures.add(executor.submit(() -> { + readyLatch.countDown(); + if (!latch.await(30, TimeUnit.SECONDS)) { + throw new RuntimeException("await timeout"); + } + return tbDate.toISOString(); + })); + } + ListenableFuture> future = Futures.allAsList(futures); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(List result) { + + } + + @Override + public void onFailure(Throwable t) { + log.error("Failure happens on iteration {}", iteration); + } + }, MoreExecutors.directExecutor()); + readyLatch.await(30, TimeUnit.SECONDS); + latch.countDown(); + future.get(30, TimeUnit.SECONDS); + } + } + + @Test + void testToISOStringThreadLocalStaticFormatter() throws ExecutionException, InterruptedException, TimeoutException { + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)); + long ts = 1709217342987L; //Thu Feb 29 2024 14:35:42.987 GMT+0000 + int offset = Calendar.getInstance().get(Calendar.ZONE_OFFSET); // for example 3600000 for GMT + 1 + TbDate tbDate = new TbDate(ts - offset); + String datePrefix = "2024-02-29T14:35:42.987"; //without time zone + assertThat(tbDate.toISOString()) + .as("format in main thread") + .startsWith(datePrefix); + assertThat(executor.submit(tbDate::toISOString).get(30, TimeUnit.SECONDS)) + .as("format in executor thread") + .startsWith(datePrefix); + assertThat(new TbDate(ts - offset).toISOString()) + .as("new instance format in main thread") + .startsWith(datePrefix); + assertThat(executor.submit(() -> new TbDate(ts - offset).toISOString()).get(30, TimeUnit.SECONDS)) + .as("new instance format in executor thread") + .startsWith(datePrefix); + } + +} diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index b6d5395af8..a16a70b962 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -27,6 +27,7 @@ import org.mvel2.SandboxedParserConfiguration; import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionHashMap; +import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -348,6 +349,24 @@ public class TbUtilsTest { Assert.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleVaList, 0, false))); } + @Test + public void parseBytesDecodeToJson() throws IOException { + String expectedStr = "{\"hello\": \"world\"}"; + ExecutionHashMap expectedJson = new ExecutionHashMap<>(1, ctx); + expectedJson.put("hello", "world"); + List expectedBytes = TbUtils.stringToBytes(ctx, expectedStr); + Object actualJson = TbUtils.decodeToJson(ctx, expectedBytes); + Assert.assertEquals(expectedJson,actualJson); + } + @Test + public void parseStringDecodeToJson() throws IOException { + String expectedStr = "{\"hello\": \"world\"}"; + ExecutionHashMap expectedJson = new ExecutionHashMap<>(1, ctx); + expectedJson.put("hello", "world"); + Object actualJson = TbUtils.decodeToJson(ctx, expectedStr); + Assert.assertEquals(expectedJson,actualJson); + } + private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 10d4ea3b95..0869243e7a 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 3e1174362c..2ac15d3e42 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapSessionMsgType.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapSessionMsgType.java new file mode 100644 index 0000000000..fcf33ea037 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapSessionMsgType.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 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.transport.coap; + +public enum CoapSessionMsgType { + + GET_ATTRIBUTES_REQUEST, + POST_ATTRIBUTES_REQUEST, + SUBSCRIBE_ATTRIBUTES_REQUEST, + UNSUBSCRIBE_ATTRIBUTES_REQUEST, + POST_TELEMETRY_REQUEST, + SUBSCRIBE_RPC_COMMANDS_REQUEST, + UNSUBSCRIBE_RPC_COMMANDS_REQUEST, + TO_DEVICE_RPC_RESPONSE, + TO_SERVER_RPC_REQUEST, + CLAIM_REQUEST; + +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index 7dde25bfd0..bd02d9fcc4 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -30,10 +30,10 @@ import org.thingsboard.server.coapserver.TbCoapDtlsSessionInfo; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.security.DeviceTokenCredentials; import org.thingsboard.server.common.msg.session.FeatureType; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.JsonConverter; @@ -120,7 +120,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } else if (exchange.getRequestOptions().hasObserve()) { processExchangeGetRequest(exchange, featureType.get()); } else if (featureType.get() == FeatureType.ATTRIBUTES) { - processRequest(exchange, SessionMsgType.GET_ATTRIBUTES_REQUEST); + processRequest(exchange, CoapSessionMsgType.GET_ATTRIBUTES_REQUEST); } else { log.trace("Invalid feature type parameter"); exchange.respond(CoAP.ResponseCode.BAD_REQUEST); @@ -129,13 +129,13 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private void processExchangeGetRequest(CoapExchange exchange, FeatureType featureType) { boolean unsubscribe = exchange.getRequestOptions().getObserve() == 1; - SessionMsgType sessionMsgType; + CoapSessionMsgType coapSessionMsgType; if (featureType == FeatureType.RPC) { - sessionMsgType = unsubscribe ? SessionMsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST : SessionMsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST; + coapSessionMsgType = unsubscribe ? CoapSessionMsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST : CoapSessionMsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST; } else { - sessionMsgType = unsubscribe ? SessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST : SessionMsgType.SUBSCRIBE_ATTRIBUTES_REQUEST; + coapSessionMsgType = unsubscribe ? CoapSessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST : CoapSessionMsgType.SUBSCRIBE_ATTRIBUTES_REQUEST; } - processRequest(exchange, sessionMsgType); + processRequest(exchange, coapSessionMsgType); } @Override @@ -147,21 +147,21 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } else { switch (featureType.get()) { case ATTRIBUTES: - processRequest(exchange, SessionMsgType.POST_ATTRIBUTES_REQUEST); + processRequest(exchange, CoapSessionMsgType.POST_ATTRIBUTES_REQUEST); break; case TELEMETRY: - processRequest(exchange, SessionMsgType.POST_TELEMETRY_REQUEST); + processRequest(exchange, CoapSessionMsgType.POST_TELEMETRY_REQUEST); break; case RPC: Optional requestId = getRequestId(exchange.advanced().getRequest()); if (requestId.isPresent()) { - processRequest(exchange, SessionMsgType.TO_DEVICE_RPC_RESPONSE); + processRequest(exchange, CoapSessionMsgType.TO_DEVICE_RPC_RESPONSE); } else { - processRequest(exchange, SessionMsgType.TO_SERVER_RPC_REQUEST); + processRequest(exchange, CoapSessionMsgType.TO_SERVER_RPC_REQUEST); } break; case CLAIM: - processRequest(exchange, SessionMsgType.CLAIM_REQUEST); + processRequest(exchange, CoapSessionMsgType.CLAIM_REQUEST); break; case PROVISION: processProvision(exchange); @@ -195,7 +195,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } } - private void processRequest(CoapExchange exchange, SessionMsgType type) { + private void processRequest(CoapExchange exchange, CoapSessionMsgType type) { log.trace("Processing {}", exchange.advanced().getRequest()); deferAccept(exchange); Exchange advanced = exchange.advanced(); @@ -218,7 +218,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } } - private void processAccessTokenRequest(CoapExchange exchange, SessionMsgType type, Request request) { + private void processAccessTokenRequest(CoapExchange exchange, CoapSessionMsgType type, Request request) { Optional credentials = decodeCredentials(request); if (credentials.isEmpty()) { exchange.respond(CoAP.ResponseCode.UNAUTHORIZED); @@ -228,7 +228,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { new CoapDeviceAuthCallback(exchange, (deviceCredentials, deviceProfile) -> processRequest(exchange, type, request, deviceCredentials, deviceProfile))); } - private void processRequest(CoapExchange exchange, SessionMsgType type, Request request, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) { + private void processRequest(CoapExchange exchange, CoapSessionMsgType type, Request request, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) { TbCoapClientState clientState = null; try { clientState = clients.getOrCreateClient(type, deviceCredentials, deviceProfile); @@ -380,12 +380,16 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } } - private Optional getFeatureType(Request request) { + protected Optional getFeatureType(Request request) { List uriPath = request.getOptions().getUriPath(); try { - if (uriPath.size() >= FEATURE_TYPE_POSITION) { + int size = uriPath.size(); + if (size >= FEATURE_TYPE_POSITION) { + if (size == FEATURE_TYPE_POSITION && StringUtils.isNumeric(uriPath.get(size - 1))) { + return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 2).toUpperCase())); + } return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase())); - } else if (uriPath.size() >= FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST) { + } else if (size == FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST) { if (uriPath.contains(DataConstants.PROVISION)) { return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase())); } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/CoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/CoapClientContext.java index 36ef8472de..a8531a7fba 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/CoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/CoapClientContext.java @@ -18,7 +18,7 @@ package org.thingsboard.server.transport.coap.client; import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.server.resources.CoapExchange; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.transport.coap.CoapSessionMsgType; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; @@ -33,7 +33,7 @@ public interface CoapClientContext { AtomicInteger getNotificationCounterByToken(String token); - TbCoapClientState getOrCreateClient(SessionMsgType type, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) throws AdaptorException; + TbCoapClientState getOrCreateClient(CoapSessionMsgType type, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) throws AdaptorException; TransportProtos.SessionInfoProto getNewSyncSession(TbCoapClientState clientState); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java index f70967b72d..26effe7058 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java @@ -45,7 +45,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.msg.session.FeatureType; -import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.transport.coap.CoapSessionMsgType; import org.thingsboard.server.common.transport.DeviceDeletedEvent; import org.thingsboard.server.common.transport.DeviceProfileUpdatedEvent; import org.thingsboard.server.common.transport.DeviceUpdatedEvent; @@ -388,7 +388,7 @@ public class DefaultCoapClientContext implements CoapClientContext { } @Override - public TbCoapClientState getOrCreateClient(SessionMsgType type, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) throws AdaptorException { + public TbCoapClientState getOrCreateClient(CoapSessionMsgType type, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) throws AdaptorException { DeviceId deviceId = deviceCredentials.getDeviceInfo().getDeviceId(); TbCoapClientState state = getClientState(deviceId); state.lock(); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java index 93061d522a..1c0a599082 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java @@ -15,13 +15,15 @@ */ package org.thingsboard.server.transport.coap.efento; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.protobuf.InvalidProtocolBufferException; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.util.TriConsumer; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; -import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.network.Exchange; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; @@ -32,8 +34,11 @@ import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransp import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.EfentoCoapDeviceTypeConfiguration; import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.adaptor.ProtoConverter; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.coap.ConfigProtos; +import org.thingsboard.server.gen.transport.coap.DeviceInfoProtos; import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos; import org.thingsboard.server.gen.transport.coap.MeasurementsProtos; import org.thingsboard.server.transport.coap.AbstractCoapTransportResource; @@ -43,13 +48,17 @@ import org.thingsboard.server.transport.coap.callback.CoapEfentoCallback; import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils; import java.nio.ByteBuffer; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import static com.google.gson.JsonParser.parseString; import static org.thingsboard.server.transport.coap.CoapTransportService.CONFIGURATION; import static org.thingsboard.server.transport.coap.CoapTransportService.CURRENT_TIMESTAMP; import static org.thingsboard.server.transport.coap.CoapTransportService.DEVICE_INFO; @@ -94,14 +103,13 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { String requestType = uriPath.get(1); switch (requestType) { case MEASUREMENTS: - processMeasurementsRequest(exchange, request); + processMeasurementsRequest(exchange); break; case DEVICE_INFO: + processDeviceInfoRequest(exchange); + break; case CONFIGURATION: - //We respond only to confirmed requests in order to reduce battery consumption for Efento devices. - if (exchange.advanced().getRequest().isConfirmable()) { - exchange.respond(new Response(CoAP.ResponseCode.CREATED)); - } + processConfigurationRequest(exchange); break; default: exchange.respond(CoAP.ResponseCode.BAD_REQUEST); @@ -109,34 +117,92 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { } } - private void processMeasurementsRequest(CoapExchange exchange, Request request) { - byte[] bytes = request.getPayload(); + private void processMeasurementsRequest(CoapExchange exchange) { + byte[] bytes = exchange.advanced().getRequest().getPayload(); try { MeasurementsProtos.ProtoMeasurements protoMeasurements = MeasurementsProtos.ProtoMeasurements.parseFrom(bytes); log.trace("Successfully parsed Efento ProtoMeasurements: [{}]", protoMeasurements.getCloudToken()); - String token = protoMeasurements.getCloudToken(); - transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(token).build(), - new CoapDeviceAuthCallback(exchange, (msg, deviceProfile) -> { - TransportProtos.SessionInfoProto sessionInfo = SessionInfoCreator.create(msg, transportContext, UUID.randomUUID()); - UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); - try { - validateEfentoTransportConfiguration(deviceProfile); - List efentoMeasurements = getEfentoMeasurements(protoMeasurements, sessionId); - transportService.process(sessionInfo, - transportContext.getEfentoCoapAdaptor().convertToPostTelemetry(sessionId, efentoMeasurements), - new CoapEfentoCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - reportSubscriptionInfo(sessionInfo, false, false); - } catch (AdaptorException e) { - log.error("[{}] Failed to decode Efento ProtoMeasurements: ", sessionId, e); - exchange.respond(CoAP.ResponseCode.BAD_REQUEST); - } - })); + validateAndProcessEffentoMessage(protoMeasurements.getCloudToken(), exchange, (deviceProfile, sessionInfo, sessionId) -> { + try { + List measurements = getEfentoMeasurements(protoMeasurements, sessionId); + transportService.process(sessionInfo, + transportContext.getEfentoCoapAdaptor().convertToPostTelemetry(sessionId, measurements), + new CoapEfentoCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + } catch (AdaptorException e) { + log.error("[{}] Failed to decode Efento ProtoMeasurements: ", sessionId, e); + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } + }); } catch (Exception e) { log.error("Failed to decode Efento ProtoMeasurements: ", e); exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); } } + private void processDeviceInfoRequest(CoapExchange exchange) { + byte[] bytes = exchange.advanced().getRequest().getPayload(); + try { + DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo = DeviceInfoProtos.ProtoDeviceInfo.parseFrom(bytes); + String token = protoDeviceInfo.getCloudToken(); + log.trace("Successfully parsed Efento ProtoDeviceInfo: [{}]", token); + validateAndProcessEffentoMessage(token, exchange, (deviceProfile, sessionInfo, sessionId) -> { + try { + EfentoTelemetry deviceInfo = getEfentoDeviceInfo(protoDeviceInfo); + transportService.process(sessionInfo, + transportContext.getEfentoCoapAdaptor().convertToPostTelemetry(sessionId, List.of(deviceInfo)), + new CoapEfentoCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + } catch (AdaptorException e) { + log.error("[{}] Failed to decode Efento ProtoDeviceInfo: ", sessionId, e); + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } + }); + } catch (Exception e) { + log.error("Failed to decode Efento ProtoDeviceInfo: ", e); + exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); + } + } + + private void processConfigurationRequest(CoapExchange exchange) { + byte[] bytes = exchange.advanced().getRequest().getPayload(); + try { + ConfigProtos.ProtoConfig protoConfig = ConfigProtos.ProtoConfig.parseFrom(bytes); + String token = protoConfig.getCloudToken(); + log.trace("Successfully parsed Efento ProtoConfig: [{}]", token); + validateAndProcessEffentoMessage(token, exchange, (deviceProfile, sessionInfo, sessionId) -> { + try { + JsonElement configuration = getEfentoConfiguration(bytes); + transportService.process(sessionInfo, + transportContext.getEfentoCoapAdaptor().convertToPostAttributes(sessionId, configuration), + new CoapEfentoCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + } catch (AdaptorException e) { + log.error("[{}] Failed to decode Efento ProtoConfig: ", sessionId, e); + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } catch (InvalidProtocolBufferException e) { + throw new RuntimeException(e); + } + }); + } catch (Exception e) { + log.error("Failed to decode Efento ProtoConfig: ", e); + exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); + } + } + + private void validateAndProcessEffentoMessage(String token, CoapExchange exchange, TriConsumer requestProcessor) { + transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(token).build(), + new CoapDeviceAuthCallback(exchange, (msg, deviceProfile) -> { + TransportProtos.SessionInfoProto sessionInfo = SessionInfoCreator.create(msg, transportContext, UUID.randomUUID()); + UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); + try { + validateEfentoTransportConfiguration(deviceProfile); + requestProcessor.accept(deviceProfile, sessionInfo, sessionId); + reportSubscriptionInfo(sessionInfo, false, false); + } catch (AdaptorException e) { + log.error("[{}] Failed to decode Efento request: ", sessionId, e); + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } + })); + } + @Override public Resource getChild(String name) { return this; @@ -155,7 +221,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { } } - private List getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) { + private List getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) { String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNum().toByteArray()); boolean batteryStatus = protoMeasurements.getBatteryStatus(); int measurementPeriodBase = protoMeasurements.getMeasurementPeriodBase(); @@ -243,6 +309,12 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); values.addProperty("ok_alarm_" + channel, data); break; + case PULSE_CNT: + values = valuesMap.computeIfAbsent(startTimestampMillis, k -> + CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); + values.addProperty("pulse_cnt_" + channel, (double) (startPoint + sampleOffset)); + startTimestampMillis = startTimestampMillis + measurementPeriodMillis; + break; case NO_SENSOR: case UNRECOGNIZED: log.trace("[{}][{}] Sensor error value! Ignoring.", sessionId, measurementTypeName); @@ -261,9 +333,9 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { throw new IllegalStateException("[" + sessionId + "]: Failed to get Efento measurements, reason: channels list is empty!"); } if (!CollectionUtils.isEmpty(valuesMap)) { - List efentoMeasurements = new ArrayList<>(); + List efentoMeasurements = new ArrayList<>(); for (Long ts : valuesMap.keySet()) { - EfentoMeasurements measurement = new EfentoMeasurements(ts, valuesMap.get(ts)); + EfentoTelemetry measurement = new EfentoTelemetry(ts, valuesMap.get(ts)); efentoMeasurements.add(measurement); } return efentoMeasurements; @@ -272,12 +344,107 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { } } + private EfentoTelemetry getEfentoDeviceInfo(DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo) { + JsonObject values = new JsonObject(); + values.addProperty("sw_version", protoDeviceInfo.getSwVersion()); + + //memory statistics + values.addProperty("nv_storage_status", protoDeviceInfo.getMemoryStatistics(0)); + values.addProperty("timestamp_of_the_end_of_collecting_statistics", getDate(protoDeviceInfo.getMemoryStatistics(1))); + values.addProperty("capacity_of_memory_in_bytes", protoDeviceInfo.getMemoryStatistics(2)); + values.addProperty("used_space_in_bytes", protoDeviceInfo.getMemoryStatistics(3)); + values.addProperty("size_of_invalid_packets_in_bytes", protoDeviceInfo.getMemoryStatistics(4)); + values.addProperty("size_of_corrupted_packets_in_bytes", protoDeviceInfo.getMemoryStatistics(5)); + values.addProperty("number_of_valid_packets", protoDeviceInfo.getMemoryStatistics(6)); + values.addProperty("number_of_invalid_packets", protoDeviceInfo.getMemoryStatistics(7)); + values.addProperty("number_of_corrupted_packets", protoDeviceInfo.getMemoryStatistics(8)); + values.addProperty("number_of_all_samples_for_channel_1", protoDeviceInfo.getMemoryStatistics(9)); + values.addProperty("number_of_all_samples_for_channel_2", protoDeviceInfo.getMemoryStatistics(10)); + values.addProperty("number_of_all_samples_for_channel_3", protoDeviceInfo.getMemoryStatistics(11)); + values.addProperty("number_of_all_samples_for_channel_4", protoDeviceInfo.getMemoryStatistics(12)); + values.addProperty("number_of_all_samples_for_channel_5", protoDeviceInfo.getMemoryStatistics(13)); + values.addProperty("number_of_all_samples_for_channel_6", protoDeviceInfo.getMemoryStatistics(14)); + values.addProperty("timestamp_of_the_first_binary_measurement", getDate(protoDeviceInfo.getMemoryStatistics(15))); + values.addProperty("timestamp_of_the_last_binary_measurement", getDate(protoDeviceInfo.getMemoryStatistics(16))); + values.addProperty("timestamp_of_the_first_binary_measurement_sent", getDate(protoDeviceInfo.getMemoryStatistics(17))); + values.addProperty("timestamp_of_the_first_continuous_measurement", getDate(protoDeviceInfo.getMemoryStatistics(18))); + values.addProperty("timestamp_of_the_last_continuous_measurement", getDate(protoDeviceInfo.getMemoryStatistics(19))); + values.addProperty("timestamp_of_the_last_continuous_measurement_sent", getDate(protoDeviceInfo.getMemoryStatistics(20))); + values.addProperty("nvm_write_counter", protoDeviceInfo.getMemoryStatistics(21)); + + //modem info + DeviceInfoProtos.ProtoModem modem = protoDeviceInfo.getModem(); + values.addProperty("modem_types", modem.getType().toString()); + values.addProperty("sc_EARNFCN_offset", modem.getParameters(0)); + values.addProperty("sc_EARFCN", modem.getParameters(1)); + values.addProperty("sc_PCI", modem.getParameters(2)); + values.addProperty("sc_Cell_id", modem.getParameters(3)); + values.addProperty("sc_RSRP", modem.getParameters(4)); + values.addProperty("sc_RSRQ", modem.getParameters(5)); + values.addProperty("sc_RSSI", modem.getParameters(6)); + values.addProperty("sc_SINR", modem.getParameters(7)); + values.addProperty("sc_Band", modem.getParameters(8)); + values.addProperty("sc_TAC", modem.getParameters(9)); + values.addProperty("sc_ECL", modem.getParameters(10)); + values.addProperty("sc_TX_PWR", modem.getParameters(11)); + values.addProperty("op_mode", modem.getParameters(12)); + values.addProperty("nc_EARFCN", modem.getParameters(13)); + values.addProperty("nc_EARNFCN_offset", modem.getParameters(14)); + values.addProperty("nc_PCI", modem.getParameters(15)); + values.addProperty("nc_RSRP", modem.getParameters(16)); + values.addProperty("RLC_UL_BLER", modem.getParameters(17)); + values.addProperty("RLC_DL_BLER", modem.getParameters(18)); + values.addProperty("MAC_UL_BLER", modem.getParameters(19)); + values.addProperty("MAC_DL_BLER", modem.getParameters(20)); + values.addProperty("MAC_UL_TOTAL_BYTES", modem.getParameters(21)); + values.addProperty("MAC_DL_TOTAL_BYTES", modem.getParameters(22)); + values.addProperty("MAC_UL_total_HARQ_Tx", modem.getParameters(23)); + values.addProperty("MAC_DL_total_HARQ_Tx", modem.getParameters(24)); + values.addProperty("MAC_UL_HARQ_re_Tx", modem.getParameters(25)); + values.addProperty("MAC_DL_HARQ_re_Tx", modem.getParameters(26)); + values.addProperty("RLC_UL_tput", modem.getParameters(27)); + values.addProperty("RLC_DL_tput", modem.getParameters(28)); + values.addProperty("MAC_UL_tput", modem.getParameters(29)); + values.addProperty("MAC_DL_tput", modem.getParameters(30)); + values.addProperty("sleep_duration", modem.getParameters(31)); + values.addProperty("rx_time", modem.getParameters(32)); + values.addProperty("tx_time", modem.getParameters(33)); + + //Runtime info + DeviceInfoProtos.ProtoRuntime runtimeInfo = protoDeviceInfo.getRuntimeInfo(); + values.addProperty("battery_reset_timestamp", getDate(runtimeInfo.getBatteryResetTimestamp())); + values.addProperty("max_mcu_temp", runtimeInfo.getMaxMcuTemperature()); + values.addProperty("mcu_temp", runtimeInfo.getMcuTemperature()); + values.addProperty("counter_of_confirmable_messages_attempts", runtimeInfo.getMessageCounters(0)); + values.addProperty("counter_of_non_confirmable_messages_attempts", runtimeInfo.getMessageCounters(1)); + values.addProperty("counter_of_succeeded_messages", runtimeInfo.getMessageCounters(2)); + values.addProperty("min_battery_mcu_temp", runtimeInfo.getMinBatteryMcuTemperature()); + values.addProperty("min_battery_voltage", runtimeInfo.getMinBatteryVoltage()); + values.addProperty("min_mcu_temp", runtimeInfo.getMinMcuTemperature()); + values.addProperty("runtime_errors", runtimeInfo.getRuntimeErrorsCount()); + values.addProperty("up_time", runtimeInfo.getUpTime()); + + return new EfentoTelemetry(System.currentTimeMillis(), values); + } + + private JsonElement getEfentoConfiguration(byte[] bytes) throws InvalidProtocolBufferException { + return parseString(ProtoConverter.dynamicMsgToJson(bytes, ConfigProtos.getDescriptor().getMessageTypes().get(2))); + } + + private static String getDate(long seconds) { + if (seconds == -1L || seconds == 4294967295L) { + return "Undefined"; + } + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd MMM yyyy HH:mm:ss Z"); + return simpleDateFormat.format(new Date(TimeUnit.SECONDS.toMillis(seconds))); + } + @Data @AllArgsConstructor - public static class EfentoMeasurements { + public static class EfentoTelemetry { private long ts; - private JsonObject values; + private JsonElement values; } } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/adaptor/EfentoCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/adaptor/EfentoCoapAdaptor.java index a6b588139a..ce75608a7d 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/adaptor/EfentoCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/adaptor/EfentoCoapAdaptor.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.coap.efento.adaptor; import com.google.gson.Gson; +import com.google.gson.JsonElement; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.adaptor.AdaptorException; @@ -32,12 +33,23 @@ public class EfentoCoapAdaptor { private static final Gson gson = new Gson(); - public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, List measurements) throws AdaptorException { + public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, List telemetryList) throws AdaptorException { try { - return JsonConverter.convertToTelemetryProto(gson.toJsonTree(measurements)); + return JsonConverter.convertToTelemetryProto(gson.toJsonTree(telemetryList)); } catch (Exception ex) { log.warn("[{}] Failed to convert EfentoMeasurements to PostTelemetry request!", sessionId); throw new AdaptorException(ex); } } + + public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, JsonElement deviceInfo) throws AdaptorException { + try { + return JsonConverter.convertToAttributesProto(deviceInfo); + } catch (Exception ex) { + log.warn("[{}] Failed to convert JsonObject to PostTelemetry request!", sessionId); + throw new AdaptorException(ex); + } + } + + } diff --git a/common/transport/coap/src/main/proto/efento/proto_config.proto b/common/transport/coap/src/main/proto/efento/proto_config.proto new file mode 100644 index 0000000000..0903494245 --- /dev/null +++ b/common/transport/coap/src/main/proto/efento/proto_config.proto @@ -0,0 +1,312 @@ +/** + * Copyright © 2016-2023 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. + */ +syntax = "proto3"; + +import "efento/proto_measurement_types.proto"; +import "efento/proto_rule.proto"; + +option java_package = "org.thingsboard.server.gen.transport.coap"; +option java_outer_classname = "ConfigProtos"; + +/* Message containing optional channels control parameters */ +message ProtoChannelControl { + + /* Channel index */ + uint32 channel_index = 1; + + /* Control parameters. Maximal number equals 4. This field is channel specific: */ + /* IO_control channel: */ + /* - control_params[0]: */ + /* - Byte 0: On state configuration */ + /* 0x01 - Low */ + /* 0x02 - High */ + /* 0x03 - High-Z (disconnected) */ + /* - Byte 1: Off state configuration */ + /* 0x01 - Low */ + /* 0x02 - High */ + /* 0x03 - High-Z (disconnected) */ + /* - Byte 2: Power on channel state */ + /* 0x01 - On */ + /* 0x02 - Off */ + repeated uint32 control_params = 2; +} + +/* Message containing request data for accesing calibration parameters */ +message ProtoCalibrationParameters { + + /* Request details. Bitmask: */ + /* - calibration_request[0:2] - requested channel number. */ + uint32 calibration_request = 1; + + /* Assignment of a channel. */ + uint32 channel_assignment = 2; + + /* Table of calibration parameters. Max size = 8. */ + repeated int32 parameters = 3; +} + +/* Main message sent in the payload. Each field in this message is independent of the others - only parameters that should be */ +/* changed need to be sent in the payload. */ +/* If the value of a selected parameter shall not be changed, do not include it in the payload */ +message ProtoConfig { + + /* DEPRECATED - Used for backward compatibility with fw versions 5.x */ + /* repeated Threshold thresholds = 1; */ + + /* 'Measurement_period_base' and 'measurement_period_factor' define how often the measurements are taken. */ + /* Sensors of 'Continuous' type take measurement each Measurement_period_base * measurement_period_factor. */ + /* Sensors of 'Binary' type take measurement each Measurement_period_base. */ + /* For backward compatibility with versions 5.x in case of binary/mixed sensors, if the 'measurement_period_factor' is */ + /* not sent (equal to 0), then the default value '14' shall be used for period calculation. */ + /* For backward compatibility with versions 5.x in case of continues sensors, if the measurement_period_factor is */ + /* not sent (equal to 0), then the default value '1' shall be used for period calculation. */ + /* measurement period base in seconds */ + /* Range [1:65535] - minimum value can vary depends on installed sensors */ + uint32 measurement_period_base = 2; + + /* Measurement period factor */ + /* Range [1:65535] - minimum value can vary depends on installed sensors */ + uint32 measurement_period_factor = 26; + + /* Transmission interval in seconds. Range: [60:604800] */ + uint32 transmission_interval = 3; + + /* BLE turnoff time in seconds. Once receiving this setting, BLE will be switched off after the set number of seconds. */ + /* If BLE is already switched off, it will switch on for the set number of seconds and switch off afterwards. */ + /* Range [60:604800] and 0xFFFFFFFF */ + /* 0xFFFFFFFF - always on */ + uint32 ble_turnoff_time = 4; + + /* ACK interval in seconds */ + /* Range [180:2592000] and 0xFFFFFFFF */ + /* 0xFFFFFFFF - always request ACK */ + uint32 ack_interval = 5; + + /* Specifies, if the additional device info is requested. If true, sensor will send a message to endpoint '/i' with the */ + /* device info. This field is only sent by server */ + bool request_device_info = 6; + + /* Specifies, if software update is available. This field is only sent by server */ + bool request_fw_update = 7; + + /* Current time in seconds sine 1st of January 1970 (epoch time). */ + uint32 current_time = 8; + + /* NB-IoT transfer limit */ + /* Range: [1:65535] */ + /* 65535 - disable transfer limit function */ + uint32 transfer_limit = 9; + + /* NB-IoT transfer limit timer in seconds */ + /* Range: [1:65535] */ + /* 65535 - disable transfer limit function */ + uint32 transfer_limit_timer = 10; + + /* Data (measurements) server IP address */ + /* IP of the data server as string in form x.x.x.x. For example: 18.184.24.239 */ + string data_server_ip = 11; + + /* Data (measurements) server port */ + /* Range: [1:65535] */ + uint32 data_server_port = 12; + + /* Update server IP address */ + /* IP of data server as string in form x.x.x.x. For example: 18.184.24.239 */ + string update_server_ip = 13; + + /* Update server port for UDP transmission */ + /* Range: [1:65535] */ + uint32 update_server_port_udp = 14; + + /* Update server port for CoAP transmission */ + /* Range: [1:65535] */ + uint32 update_server_port_coap = 15; + + /* APN as string. Max length 49 */ + /* String with special character 0x7F (DEL) only indicates that automatic apn is turn on */ + string apn = 16; + + /* PLMN selection */ + /* Range: [100:999999] */ + /* 0xFFFFFFFF or 1000000 - automatic selection */ + uint32 plmn_selection = 17; + + /* Device will power off its cellular modem for requested number of seconds. Maximum number of seconds 604800 (7 days) */ + /* This field is only sent by server */ + uint32 disable_modem_request = 18; + + /* If set, the device will send its configuration to the endpoint '/c' as a confirmable message */ + /* This field is only sent by server */ + bool request_configuration = 19; + + /* Device's error codes. */ + /* This field is only sent by device */ + repeated uint32 errors = 20; + + /* Identifier of current configuration - Every change of the configuration results in change of the value of this field */ + /* This field is only sent by device */ + uint32 hash = 21; + + /* If true, the device will accept the configuration without functional testing (eg. network connection) */ + bool accept_without_testing = 22; + + /* Cloud token configuration: */ + /* - 1: cloud token set to the value of cloud_token field */ + /* - 2: cloud token set to IMEI of the cellular module */ + /* - 255: do not send cloud_token field */ + uint32 cloud_token_config = 23; + + /* Cloud token that should be sent with each measurement frame */ + string cloud_token = 24; + + /* Serial number of the device */ + /* This field is only sent by device */ + bytes serial_number = 25; + + /* Type of channel */ + /* This field is only sent by device */ + repeated MeasurementType channel_types = 27; + + /* Edge logic rules set on the device. Up to 12 rules are supported */ + repeated ProtoRule rules = 28; + + /* Supervision period */ + /* Range: [180:604800] */ + /* 0xFFFFFFFF - Functionality disabled */ + uint32 supervision_period = 29; + + /* If true, sensor's measurement memory will be erased */ + bool memory_reset_request = 30; + + /* Bytes 0-4 - Band selection mask. Mask = 1 << position */ + /* Band | 1 | 2 | 3 | 4 | 5 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 25 | 26 | 28 | 66 | 71 | 85 | */ + /* Position: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | */ + /* example: To enable band 3, 8 and 20 set to (1 << 2) + (1 << 5) + (1 << 11) = 2084 */ + uint32 modem_bands_mask = 31; + + /* Data endpoint (string - max length 16) */ + string data_endpoint = 32; + + /* Configuration endpoint (string - max length 16) */ + string configuration_endpoint = 33; + + /* Device info endpoint (string - max length 16) */ + string device_info_endpoint = 34; + + /* Time endpoint (string - max length 16) */ + string time_endpoint = 35; + + /* Bluetooth TX power level. Value is the index of the absolute value of TX power, that depends on the BLE module */ + /* Range: [1:4] */ + uint32 ble_tx_power_level = 36; + + /* Deprecated field */ + /* If true, the sensor's runtime errors will be cleared */ + bool request_runtime_errors_clear = 37; + + /* Timestamp when a new error code was reported */ + uint32 error_timestamp = 38; + + /* Timestamp when the new configuration was set */ + uint32 hash_timestamp = 39; + + /* Cloud token CoAP option ID: */ + /* - [1:64999] - CoAP option ID containing cloud token */ + /* - 65000 - cloud token sent in the payload */ + uint32 cloud_token_coap_option = 40; + + /* ECDSA payload signature CoAP option ID: */ + /* - [1:64999] - CoAP option ID containing ECDSA payload signature */ + /* - 65000 - no payload signature in CoAP option */ + uint32 payload_signature_coap_option = 41; + + /* DNS server IP address grouped in the array as four octets. Set 255.255.255.255 to use a network DNS server */ + /* Note: when setting less than four octets the remaining will be filled with zeros. */ + repeated uint32 dns_server_ip = 42; + + /* DNS TTL configuration: */ + /* - [1:864000] - custom TTL in seconds (additionally, the DNS request when communication has failed) */ + /* - 864001 - accept TTL from the DNS server (additionally, the DNS request when communication has failed) */ + /* - 864002 - DNS request is only after communication failed */ + uint32 dns_ttl_config = 43; + + /* Configuration payload split information. Information about dividing the payload into parts */ + /* values < 0 - payload has been split, expect another part of the payload in the next message. */ + /* The absolute value indicates an index of the current message. */ + /* value = 0 - payload has not been splitted */ + /* values > 0 - last part of the split payload, the value indicates the total number of the messages sent */ + sint32 payload_split_info = 44; + + /* Modem update request (string - max length 48) */ + /* This field is only sent by server */ + /* For BC66 module, this field is a DFOTA URL */ + string modem_update_request = 45; + + /* Cellular configuration parameters. */ + /* 1st item - Number of used cellular parameters */ + /* 2nd - 12th items - Cellular parameters */ + repeated uint32 cellular_config_params = 46; + + /* Calendar configuration. Up to 6 calendars are supported */ + repeated ProtoCalendar calendars = 47; + + /* Control parameters for channels. Maximal number of requests equals 6 */ + /* This field is only sent by server */ + repeated ProtoChannelControl channels_control_request = 48; + + /* Set/get calibration parameters for single channel. */ + ProtoCalibrationParameters calibration_parameters_request = 49; + + /* LED behaviour configuration: */ + /* Period of LEDs flashing (5-600 seconds in 5 seconds resolution): */ + /* - led_config[0] - green LED */ + /* - led_config[1] - red LED */ + /* Time from entering the normal state, after which the LED indication is turned off */ + /* (0-240 minutes in 1 minute resolution, or 255 for always turned on): */ + /* - led_config[2] - flashing red led on communication problem */ + /* - led_config[3] - flashing red led on a sensor problem */ + /* - led_config[4] - flashing red led on a low power */ + /* - led_config[5] - flashing green led on measurement */ + /* - led_config[6] - flashing green led on transmission */ + /* - led_config[7] - flashing green led to indicate sensor's proper operation */ + /* - led_config[8] - Blink duration (20-1000ms in 5 ms resolution) */ + repeated uint32 led_config = 50; + + /* Network troubleshooting configuration, if bluetooth is turned off and communication with the server is faulty, */ + /* bluetooth will be automatically turned on until the connection is stabilized */ + /* - 1: network troubleshooting disabled */ + /* - 2: network troubleshooting enabled */ + uint32 network_troubleshooting = 51; + + /* Reserved by gateway client */ + reserved 52, 53; + + /* Encryption key configuration. Sensor sends in this field two last bytes of SHA256 hash calculated from its current */ + /* encryption_key configuration. */ + /* Max length: 16 bytes. */ + /* 0x7F - encryption key disabled. */ + bytes encryption_key = 54; + + /* User name as string. Max length 31 */ + /* String with special character 0x7F (DEL) only indicates that automatic user name is turn on */ + /* User name can only be set to custom value if apn has been configured (is not automatic) */ + string apn_user_name = 55; + + /* Password as string. Max length 31 */ + /* String with special character 0x7F (DEL) only indicates that automatic password is turn on */ + /* Password can only be set to custom value if apn_user_name has been configured (is not automatic) */ + string apn_password = 56; +} \ No newline at end of file diff --git a/common/transport/coap/src/main/proto/efento/proto_device_info.proto b/common/transport/coap/src/main/proto/efento/proto_device_info.proto new file mode 100644 index 0000000000..27dc73c25b --- /dev/null +++ b/common/transport/coap/src/main/proto/efento/proto_device_info.proto @@ -0,0 +1,197 @@ +/** + * Copyright © 2016-2023 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. + */ +syntax = "proto3"; + +option java_package = "org.thingsboard.server.gen.transport.coap"; +option java_outer_classname = "DeviceInfoProtos"; + +enum ModemType +{ + + /* Invalid value */ + MODEM_TYPE_UNSPECIFIED = 0; + + /* Quectel BC66 modem */ + MODEM_TYPE_BC66 = 1; + + /* Quectel BC66-NA modem */ + MODEM_TYPE_BC66NA = 2; +} + +message ProtoRuntime +{ + + /* Up-time in seconds (since reset) */ + uint32 up_time = 1; + + /* Message counters (since reset). There are 3 counters: */ + /* message_counters[0] - Counter of confirmable messages attempts */ + /* message_counters[1] - Counter of non-confirmable messages attempts */ + /* message_counters[2] - Counter of succeeded messages */ + repeated uint32 message_counters = 2; + + /* MCU temperature in Celsius */ + sint32 mcu_temperature = 3; + + /* Minimum battery voltage in mV */ + uint32 min_battery_voltage = 4; + + /* MCU temperature in Celsius, while the minimum battery voltage was reached */ + sint32 min_battery_mcu_temperature = 5; + + /* Battery reset timestamp (Unix timestamp) */ + uint32 battery_reset_timestamp = 6; + + /* Max MCU temperature in Celsius */ + sint32 max_mcu_temperature = 7; + + /* Min MCU temperature in Celsius */ + sint32 min_mcu_temperature = 8; + + /* Table of runtime errors. Max length: 20 */ + repeated uint32 runtime_errors = 9; +} + +message ProtoModem +{ + + ModemType type = 1; + + /* Parameters for BC66 modem: */ + /* parameters[0] - sc_EARFCN - Range: [0:262143]. Unknown value: -1 */ + /* parameters[1] - sc_EARNFCN_offset - Range: [0:4] mapped to [-2, -1, -0.5, 0, 1]. Unknown value: -1 */ + /* parameters[2] - sc_PCI - Range: [0:502]. Unknown value: -1 */ + /* parameters[3] - sc_Cell id - Range: [1:268435456]. Unknown value: 0 */ + /* parameters[4] - sc_RSRP - [dBm] - Range: [-140:-44]. Unknown value: 0 */ + /* parameters[5] - sc_RSRQ - [dB] - Range: [-20:-3]. Unknown value: 0 */ + /* parameters[6] - sc_RSSI - [dBm] - Range: [-110:-3] Unknown value: 0 */ + /* parameters[7] - sc_SINR - [dB] - Range: [-10:30]. Unknown value: 31 */ + /* parameters[8] - sc_Band - Range: [see module supported bands]. The current serving cell band. Unknown value: -1 */ + /* parameters[9] - sc_TAC - Range: [0:65536]. Unknown value: -1 */ + /* parameters[10] - sc_ECL - Range: [0:2]. Unknown value: -1 */ + /* parameters[11] - sc_TX_PWR - [0.1cBm] - Range [-440:230]. Unknown value: -1000 */ + /* parameters[12] - OP_MODE - Range: [0:3]. Unknown value: -1 */ + /* parameters[13] - nc_EARFCN - Range: [0:262143]. Unknown value: -1 */ + /* parameters[14] - nc_EARNFCN_offset - Range: [0:4] mapped to [-2, -1, -0.5, 0, 1]. Unknown value: -1 */ + /* parameters[15] - nc_PCI - Range: [0:502]. Unknown value: -1 */ + /* parameters[16] - nc_RSRP - [dBm] - Range: [-140:-44]. Unknown value: 0 */ + /* parameters[17] - RLC_UL_BLER - Range: [0:100]. Unknown value: -1 */ + /* parameters[18] - RLC_DL_BLER - Range: [0:100]. Unknown value: -1 */ + /* parameters[19] - MAC_UL_BLER - Range: [0:100]. Unknown value: -1 */ + /* parameters[20] - MAC_DL_BLER - Range: [0:100]. Unknown value: -1 */ + /* parameters[21] - MAC_UL_TOTAL_BYTES - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[22] - MAC_DL_TOTAL_BYTES - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[23] - MAC_UL_total_HARQ_Tx - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[24] - MAC_DL_total_HARQ_Tx - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[25] - MAC_UL_HARQ_re_Tx - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[26] - MAC_DL_HARQ_re_Tx - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[27] - RLC_UL_tput - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[28] - RLC_DL_tput - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[29] - MAC_UL_tput - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[30] - MAC_DL_tput - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[31] - sleep_duration - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[32] - Rx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[33] - Tx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */ + repeated sint32 parameters = 2; +} + +message ProtoUpdateInfo +{ + + /* Timestamp of update (Unix timestamp) */ + uint32 timestamp = 1; + + /* Update status, possible values: */ + /* - 0x1 - No update yet */ + /* - 0x2 - No error */ + /* - 0x3 - UDP socekt error */ + /* - 0x4 - Hash error */ + /* - 0x5 - Missing packet error */ + /* - 0x6 - Invalid data error */ + /* - 0x7 - Sending timeout error */ + /* - 0x8 - No SW to update error */ + /* - 0x9 - Sending unexpected error */ + /* - 0x10 - Unexpected error */ + uint32 status = 2; +} + +message ProtoDeviceInfo +{ + + /* Serial number of device */ + bytes serial_num = 1; + + /* Deprecated field */ + reserved 2; + + /* Software version e.g ver 06.10 -> 0x060A -> 1546 */ + uint32 sw_version = 3; + + /* Deprecated fields */ + reserved 4,5,6,7,8,9,10,11,12; + + /* Structure with battery and temperature information */ + ProtoRuntime runtime_info = 13; + + /* Structure with modem specific runtime information */ + ProtoModem modem = 14; + + /* String up to 7 bytes long. Software commit id e.g. "e0e8556" */ + /* From version 06.07 the first two characters indicate the LTS version. */ + /* For example: the value "0bdd23f" means LTS version 11 and the beginning of the commit ID "dd23f" */ + string commit_id = 15; + + /* Optional string up to 36 bytes long. Can be set to any user define value or hold device's IMEI */ + string cloud_token = 16; + + /* Memory statistics: */ + /* memory_statistics[0] - Status of Nv storage: */ + /* - 0 - Nv storage hasn't errors */ + /* - 1 - Nv storage has some corrupted packet. Memory is read-only */ + /* - 2 - Nv storage is corrupted. Memory is unavailable */ + /* memory_statistics[1] - Timestamp of the end of collecting statistics. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[2] - Capacity of memory in bytes */ + /* memory_statistics[3] - Used space in bytes */ + /* memory_statistics[4] - Size of invalid (outdated) packets in bytes */ + /* memory_statistics[5] - Size of corrupted packets in bytes */ + /* memory_statistics[6] - Number of valid packets */ + /* memory_statistics[7] - Number of invalid (outdated) packets */ + /* memory_statistics[8] - Number of corrupted packets */ + /* memory_statistics[9] - Number of all samples for channel 1 (valid packets) */ + /* memory_statistics[10] - Number of all samples for channel 2 (valid packets) */ + /* memory_statistics[11] - Number of all samples for channel 3 (valid packets) */ + /* memory_statistics[12] - Number of all samples for channel 4 (valid packets) */ + /* memory_statistics[13] - Number of all samples for channel 5 (valid packets) */ + /* memory_statistics[14] - Number of all samples for channel 6 (valid packets) */ + /* memory_statistics[15] - Timestamp of the first binary measurement. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[16] - Timestamp of the last binary measurement. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[17] - Timestamp of the last binary measurement, that marked as sent. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[18] - Timestamp of the first continuous measurement. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[19] - Timestamp of the last continuous measurement. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[20] - Timestamp of the last continuous measurement, that marked as sent. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[21] - NVM write counter */ + repeated uint32 memory_statistics = 17; + + /* Information about last sensor SW update */ + ProtoUpdateInfo last_update_info = 18; +} \ No newline at end of file diff --git a/common/transport/coap/src/main/proto/proto_measurement_types.proto b/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto similarity index 100% rename from common/transport/coap/src/main/proto/proto_measurement_types.proto rename to common/transport/coap/src/main/proto/efento/proto_measurement_types.proto diff --git a/common/transport/coap/src/main/proto/proto_measurements.proto b/common/transport/coap/src/main/proto/efento/proto_measurements.proto similarity index 99% rename from common/transport/coap/src/main/proto/proto_measurements.proto rename to common/transport/coap/src/main/proto/efento/proto_measurements.proto index 27bd6e3313..fd93b38b82 100644 --- a/common/transport/coap/src/main/proto/proto_measurements.proto +++ b/common/transport/coap/src/main/proto/efento/proto_measurements.proto @@ -14,7 +14,7 @@ * limitations under the License. */ syntax = "proto3"; -import "proto_measurement_types.proto"; +import "efento/proto_measurement_types.proto"; option java_package = "org.thingsboard.server.gen.transport.coap"; option java_outer_classname = "MeasurementsProtos"; diff --git a/common/transport/coap/src/main/proto/efento/proto_rule.proto b/common/transport/coap/src/main/proto/efento/proto_rule.proto new file mode 100644 index 0000000000..9a4a3cd0d1 --- /dev/null +++ b/common/transport/coap/src/main/proto/efento/proto_rule.proto @@ -0,0 +1,267 @@ +/** + * Copyright © 2016-2023 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. + */ +syntax = "proto3"; + +option java_package = "org.thingsboard.server.gen.transport.coap"; +option java_outer_classname = "ProtoRuleProtos"; + +/* Encoding A: used to set absolute values in the Rules (e.g. upper and lower threshold values) */ +/* - TEMPERATURE - [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2:4000.0]. */ +/* - HUMIDITY - [% RH] - Relative humidity. Resolution 1%. Range [0:100]. */ +/* - ATMOSPHERIC_PRESSURE - [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0:2000.0]. */ +/* - DIFERENTIAL_PRESSURE - [Pa] - Pascal. Resolution 1Pa. Range [-10000:10000] */ +/* - OK/ALARM - Not applicable */ +/* - IAQ - [IAQ] - IAQ index. Resolution 1IAQ. Range [0:500]. */ +/* - FLOODING - Not applicable */ +/* - PULSE_CNT - [NB] Number of pulses. Resolution 1 pulse. Range [0:8000000]. */ +/* - ELECTRICITY_METER - [W] - Watt; Resolution 1W. Range [0:8000000]. Average power consumption in period */ +/* - WATER_METER [l/min] - Liter per minute. Resolution 1l/min. Range [0:8000000]. Average water flow in period. */ +/* - SOIL_MOISTURE - [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000:0]. Soil moisture (tension). */ +/* - CO_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon monoxide concentration. */ +/* - NO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Nitrogen dioxide concentration. */ +/* - H2S_GAS - [ppm] - Parts per million. Resolution 0.01ppm. Range [0.00:80000.00]. Hydrogen sulfide concentration. */ +/* - AMBIENT_LIGHT -[lx] - Lux. Resolution 0.1lx. Range [0.0:100000.0]. Illuminance. */ +/* - PM_1_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0:1000]. */ +/* - PM_2_5 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0:1000]. */ +/* - PM_10_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0:1000]. */ +/* - NOISE_LEVEL - [dB] - Decibels. Resolution 0.1 dB. Range: [0.0:200.0]. Noise level. */ +/* - NH3_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Ammonia concentration. */ +/* - CH4_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Methane concentration. */ +/* - HIGH_PRESSURE - [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [0:200000]. Pressure. */ +/* - DISTANCE_MM - [mm] - Millimeter. Resolution 1mm. Range [0:100000]. Distance. */ +/* - WATER_METER_ACC_MINOR - [l] - Liter. Resolution 1l. Range [0:1000000]. Accumulative water meter (minor). */ +/* - WATER_METER_ACC_MAJOR - [hl] - Hectoliter. Resolution 1hl. Range [0:1000000]. Accumulative water meter (major). */ +/* - CO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide concentration. */ +/* - HUMIDITY ACCURATE - [% RH] - Relative humidity. Resolution 0.1%. Range [0.0:100.0]. */ +/* - STATIC_IAQ - [sIAQ] - Static IAQ index. Resolution 1sIAQ. Range [0:10000]. */ +/* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide equivalent. */ +/* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */ +/* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. */ +/* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [0:1000000]. Accumulative pulse counter (minor). */ +/* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. */ +/* Accumulative pulse counter (major). */ +/* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [0:1000000]. Accumulative electricity meter (minor). */ +/* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [0:1000000]. Accumulative electricity meter (major). */ + +/* Encoding R: used to set relative values in the Rules (e.g. differential threshold and hysteresis) */ +/* - TEMPERATURE - [°C] - Celsius degree. Resolution 0.1°C. Range [0.1:4273.2]. */ +/* - HUMIDITY - [% RH] - Relative humidity. Resolution 1%. Range [1:100]. */ +/* - ATMOSPHERIC_PRESSURE - [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [0.1:1999.0]. */ +/* - DIFERENTIAL_PRESSURE - [Pa] - Pascal. Resolution 1Pa. Range [1:20000] */ +/* - OK/ALARM - Not applicable */ +/* - VOC - [IAQ] - Iaq index. Resolution 1IAQ. Range [1:500]. */ +/* - FLOODING - Not applicable */ +/* - PULSE_CNT - [NB] Number of pulses. Resolution 1 pulse. Range [1:8000000]. */ +/* - ELECTRICITY_METER - [W] - Watt; Resolution 1W. Range [1:8000000]. Average power consumption in period */ +/* - WATER_METER [l/min] - Liter per minute. Resolution 1l/min. Range [1:8000000]. Average water flow in period. */ +/* - SOIL_MOISTURE - [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [1:1000]. Soil moisture (tension). */ +/* - CO_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon monoxide concentration. */ +/* - NO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Nitrogen dioxide concentration. */ +/* - H2S_GAS - [ppm] - Parts per million. Resolution 0.01ppm. Range [0.01:80000.00]. Hydrogen sulfide concentration. */ +/* - AMBIENT_LIGHT -[lx] - Lux. Resolution 0.1lx. Range [0.1:100000.0]. Illuminance. */ +/* - PM_1_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [1:1000]. */ +/* - PM_2_5 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [1:1000]. */ +/* - PM_10_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [1:1000]. */ +/* - NOISE_LEVEL - [dB] - Decibels. Resolution 0.1 dB. Range: [0.1:200.0]. Noise level. */ +/* - NH3_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Ammonia concentration. */ +/* - CH4_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Methane concentration. */ +/* - HIGH_PRESSURE - [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [1:200000]. Pressure. */ +/* - DISTANCE_MM - [mm] - Millimeter. Resolution 1mm. Range [1:100000]. Distance. */ +/* - WATER_METER_ACC_MINOR - [l] - Liter. Resolution 1l. Range [1:1000000]. Accumulative water meter (minor). */ +/* - WATER_METER_ACC_MAJOR - [hl] - Hectoliter. Resolution 1hl. Range [1:1000000]. Accumulative water meter (major). */ +/* - CO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon dioxide concentration. */ +/* - HUMIDITY ACCURATE - [% RH] - Relative humidity. Resolution 0.1%. Range [0.1:100.0]. */ +/* - STATIC_IAQ - [sIAQ] - Static IAQ index. Resolution 1sIAQ. Range [1:10000]. */ +/* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon dioxide equivalent. */ +/* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [1:100000]. Breath VOC estimate. */ +/* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.01:100.00]. */ +/* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [1:1000000]. Accumulative pulse counter (minor). */ +/* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [1:1000000]. */ +/* Accumulative pulse counter (major). */ +/* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [1:1000000]. Accumulative electricity meter (minor). */ +/* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [1:1000000]. Accumulative electricity meter (major). */ + +/* Condition to be checked by the device. If the condition is true, an action is triggered */ +enum Condition { + + /* Invalid value */ + CONDITION_UNSPECIFIED = 0; + + /* Threshold function for given rule_id is disabled */ + CONDITION_DISABLED = 1; + + /* Upper threshold. Continuous sensors only. If the measurement (or average from a few measurements) is over the threshold, */ + /* an action is triggered. */ + /* parameter[0] - Threshold value in "Encoding A" format. Must match channel type */ + /* parameter[1] - Hysteresis value in "Encoding R" format. Must much channel type. Set to "0" to disable */ + /* parameter[2] - Triggering mode: */ + /* - 1 - moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) */ + /* - 2 - window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) */ + /* - 3 - consecutive samples (number of consecutive samples above threshold) */ + /* parameter[3] - Number of measurements for trigger determination. E.g parameter[3] equals 3, average value from three */ + /* samples will be calculated and compared to the threshold value in average mode or the third consecutive */ + /* sample above threshold will trigger action in consecutive mode. Range: [1:10]. */ + /* parameter[4] - Type of measurement (as described in MeasurementType). */ + CONDITION_HIGH_THRESHOLD = 2; + + /* Lower threshold. Continuous sensors only. If the measurement (or average from a few measurements) is below the threshold, */ + /* an action is triggered. */ + /* parameter[0] - Threshold value in "Encoding A" format. Must match channel type */ + /* parameter[1] - Hysteresis value in "Encoding R" format. Must much channel type. Set to "0" to disable */ + /* parameter[2] - Triggering mode: */ + /* - 1 - moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) */ + /* - 2 - window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) */ + /* - 3 - consecutive samples (number of consecutive samples above threshold) */ + /* parameter[3] - Number of measurements for trigger determination. E.g parameter[3] equals 3, average value from three */ + /* samples will be calculated and compared to the threshold value in average mode or the third consecutive */ + /* sample below threshold will trigger action in consecutive mode. Range: [1:10]. */ + /* parameter[4] - Type of measurement (as described in MeasurementType). */ + CONDITION_LOW_THRESHOLD = 3; + + /* Differential threshold. Continuous sensors only. If the absolute value of the difference between the last value sent to */ + /* the server and the measurement value (or average from a few measurements) is greater or equal to the value of */ + /* the threshold set, an action is triggered. */ + /* parameter[0] - Threshold value in "Encoding R" format. Must match channel type */ + /* parameter[1] - Triggering mode: */ + /* - 1 - moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) */ + /* - 2 - window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) */ + /* - 3 - consecutive samples (number of consecutive samples above threshold) */ + /* parameter[2] - Number of measurements for trigger determination. E.g parameter[3] equals 3, average value from three */ + /* samples will be calculated and compared to the threshold value in average mode or the third consecutive */ + /* sample exceeding threshold will trigger action in consecutive mode. Range: [1:10]. */ + /* parameter[3] - Type of measurement (as described in MeasurementType). */ + CONDITION_DIFF_THRESHOLD = 4; + + /* Change of binary sensor's state. Binary sensors only. Each change of the binary's sensor state will trigger an action. */ + CONDITION_BINARY_CHANGE_STATE = 5; + + /* Logic operator. Used for combining multiple rules into more complex conditions. If the logic condition specified by */ + /* parameters (logic operator and selected rules) is met, an action is triggered. */ + /* parameter[0] - Logic operator (as described in LogicOperation). */ + /* parameter[1] - Rule selector (bit mask). Specifies which rules should be taken into account while determining */ + /* rules outcome. */ + /* parameter[2] - Rule negation (bit mask). Specifies which of chosen in parameter[1] rules should be negated */ + /* before determining rules outcome. */ + /* parameter[3] - Rule action delay [s]. Specifies time delay between the rule activation and rule action being triggered. */ + /* Range: [0:864000]. */ + /* parameter[4] - Rule return delay [s]. Specifies time delay between the rule deactivation and rule action being triggered. */ + /* Range: [0:864001]. Max parameter value disables action triggering on rule deactivation. */ + CONDITION_LOGIC_OPERATOR = 6; + + /* On measurement. Continous sensors only. The basic function is to trigger communication after measurement if at least 60s */ + /* have passed since the last one. Transmission may occur every x measurement. Optionally dependency on the other rule can */ + /* be configured, then, when all conditions are met, transmission is triggered. */ + /* parameter[0] - Send every n measurement. This parameter specifies every which measurement transmission will be triggered */ + /* if all other conditions are met. Range: [1:500]. If parameter[0] equals 1, transmission will occur after */ + /* every measurement. */ + /* parameter[1] - Optional. Rule selector (bit mask). Specifies which rule should be taken into account while determining */ + /* the measurement rule outcome. */ + /* parameter[2] - Optional. Rule negation (bit mask). Specifies which of chosen in parameter[1] rule should be negated */ + /* before determining the measurement rule outcome. */ + CONDITION_ON_MEASUREMENT = 7; +} + +/* Logic operators to be used for determining the outcome of rules with logic operator condition. */ +enum LogicOperator { + + /* Invalid use */ + LOGIC_OPERATOR_UNSPECIFIED = 0; + + /* Logic AND */ + LOGIC_OPERATOR_AND = 1; + + /* Logic OR */ + LOGIC_OPERATOR_OR = 2; +} + +/* Action to be triggered. Currently the only possible action is to trigger the transmission. */ +/* Other actions will be available in next SW releases. */ +enum Action { + + /* Invalid value */ + ACTION_UNSPECIFIED = 0; + + /* To trigger the transmission */ + ACTION_TRIGGER_TRANSMISSION = 1; + + /* To take no action. Possible for logic operator components */ + ACTION_NO_ACTION = 2; + + /* To trigger the transmission with ACK */ + ACTION_TRIGGER_TRANSMISSION_WITH_ACK = 3; +} + +/* Type of a rule calendars. */ +enum CalendarType { + + /* Invalid value */ + CALENDAR_TYPE_UNSPECIFIED = 0; + + /* Type for inactive calendars */ + CALENDAR_TYPE_DISABLED = 1; + + /* Week type. Enables selcted rules on specified days of the week in specified time periods. */ + /* parameter[0] - Week day mask. Bitmask of days when selected rules are enabled */ + /* - Bit 0 - Sunday */ + /* - Bit 1 - Monday */ + /* ... */ + /* - Bit 6 - Saturday */ + /* parameter[1] - 'From time' - point in time from which selected rules will be enabled (in minutes from midnight). */ + /* parameter[2] - 'To time' - point in time from which selected rules will be disabled (in minutes from midnight). */ + /* Note: if 'From time' is bigger than 'To time' there are two periods when rules are enabled - from 00:00 to 'To time' */ + /* and from 'From time' to 23:59. */ + /* parameter[3] - Timezone - desired timezone for date comparison. Encoded as number (N) of 15 minutes offsets */ + /* - example - if N = 4, then offset = 4 * 15min = 1h. I.e. timezone is UTC+1. */ + CALENDAR_TYPE_WEEK = 2; +} + +/* Rules calendars. Used for enabling/disabling rules based on date/time. */ +/* It is possible to configure up to 6 calendars. Each of them can affect any number of rules. */ +message ProtoCalendar { + + /* Bit mask of selected rules. Mask on bits [0:11] */ + /* - Bit 0 - Rule ID 0 */ + /* - Bit 1 - Rule ID 1 */ + /* ... */ + /* - Bit 11 - Rule ID 11 */ + uint32 rule_mask = 1; + + /* Calendars's parameters. Described in Type. */ + repeated sint32 parameters = 2; + + /* Calendar's type. Described in Type. */ + CalendarType type = 3; +} + +/* Rules used to define edge logic on the device. Rules are defined by conditions and actions: */ +/* If Condition is true, trigger Action. It is possible to configure up to 12 rules and assign them to different channels. */ +/* One rule can be assigned to any number of channels. For instance rule "If temperature is over 10 C, trigger the transmission"*/ +/* can be assigned to channels 1 and 2. No matter to how many channels a rule is assigned, it's still counted as one rule. */ +message ProtoRule { + + /* Channels to which the rule is assigned. One rule can be assigned to multiple channels as long as those are of the same type*/ + /* Bit mask on bits [0:5]. E.g. To assign the rule for channel 1: "000001", to assign rule to channels 2 and 4: "001010" */ + uint32 channel_mask = 1; + + /* Rule's condition (as described in Condition). */ + Condition condition = 2; + + /* Condition's parameters (as described in Condition). For binary sensors there are no parameters */ + repeated sint32 parameters = 3; + + /* Action to be triggered. */ + Action action = 4; +} \ No newline at end of file diff --git a/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapTransportResourceTest.java b/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapTransportResourceTest.java new file mode 100644 index 0000000000..c7f33e3694 --- /dev/null +++ b/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapTransportResourceTest.java @@ -0,0 +1,161 @@ +/** + * Copyright © 2016-2023 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.transport.coap; + +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.OptionSet; +import org.eclipse.californium.core.coap.Request; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.thingsboard.server.coapserver.CoapServerService; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.msg.session.FeatureType; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.queue.scheduler.SchedulerComponent; +import org.thingsboard.server.transport.coap.client.CoapClientContext; + +import java.util.Random; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CoapTransportResourceTest { + + private static final String V1 = "v1"; + private static final String API = "api"; + private static final String TELEMETRY = "telemetry"; + private static final String ATTRIBUTES = "attributes"; + private static final String RPC = "rpc"; + private static final String CLAIM = "claim"; + private static final String PROVISION = "provision"; + private static final String GET_ATTRIBUTES_URI_QUERY = "clientKeys=attribute1,attribute2&sharedKeys=shared1,shared2"; + + private static final Random RANDOM = new Random(); + + private static CoapTransportResource coapTransportResource; + + @BeforeAll + static void setUp() { + + var ctxMock = mock(CoapTransportContext.class); + var coapServerServiceMock = mock(CoapServerService.class); + var transportServiceMock = mock(TransportService.class); + var clientContextMock = mock(CoapClientContext.class); + var schedulerComponentMock = mock(SchedulerComponent.class); + + when(ctxMock.getTransportService()).thenReturn(transportServiceMock); + when(ctxMock.getClientContext()).thenReturn(clientContextMock); + when(ctxMock.getSessionReportTimeout()).thenReturn(1L); + when(ctxMock.getScheduler()).thenReturn(schedulerComponentMock); + + coapTransportResource = new CoapTransportResource(ctxMock, coapServerServiceMock, V1); + } + + @ParameterizedTest + @MethodSource("provideRequestAndFeatureType") + void givenRequest_whenGetFeatureType_thenReturnedExpectedFeatureType(Request request, FeatureType expectedFeatureType) { + var featureTypeOptional = coapTransportResource.getFeatureType(request); + + assertTrue(featureTypeOptional.isPresent(), "Optional is empty"); + assertEquals(expectedFeatureType, featureTypeOptional.get(), "Feature type is invalid"); + } + + static Stream provideRequestAndFeatureType() { + return Stream.of( + // accessToken based tests + Arguments.of(toAccessTokenRequest(CoAP.Code.POST, TELEMETRY), FeatureType.TELEMETRY), + Arguments.of(toAccessTokenRequest(CoAP.Code.POST, ATTRIBUTES), FeatureType.ATTRIBUTES), + Arguments.of(toGetAttributesAccessTokenRequest(), FeatureType.ATTRIBUTES), + Arguments.of(toAccessTokenRequest(CoAP.Code.GET, ATTRIBUTES), FeatureType.ATTRIBUTES), + Arguments.of(toAccessTokenRequest(CoAP.Code.GET, RPC), FeatureType.RPC), + Arguments.of(toRpcResponseAccessTokenRequest(), FeatureType.RPC), + Arguments.of(toAccessTokenRequest(CoAP.Code.POST, RPC), FeatureType.RPC), + Arguments.of(toAccessTokenRequest(CoAP.Code.POST, CLAIM), FeatureType.CLAIM), + // certificate based tests + Arguments.of(toCertificateRequest(CoAP.Code.POST, TELEMETRY), FeatureType.TELEMETRY), + Arguments.of(toCertificateRequest(CoAP.Code.POST, ATTRIBUTES), FeatureType.ATTRIBUTES), + Arguments.of(toGetAttributesCertificateRequest(), FeatureType.ATTRIBUTES), + Arguments.of(toCertificateRequest(CoAP.Code.GET, ATTRIBUTES), FeatureType.ATTRIBUTES), + Arguments.of(toCertificateRequest(CoAP.Code.GET, RPC), FeatureType.RPC), + Arguments.of(toRpcResponseCertificateRequest(), FeatureType.RPC), + Arguments.of(toCertificateRequest(CoAP.Code.POST, RPC), FeatureType.RPC), + Arguments.of(toCertificateRequest(CoAP.Code.POST, CLAIM), FeatureType.CLAIM), + // provision request + Arguments.of(toProvisionRequest(), FeatureType.PROVISION) + ); + } + + private static Request toAccessTokenRequest(CoAP.Code method, String featureType) { + return getAccessTokenRequest(method, featureType, null, null); + } + + private static Request toGetAttributesAccessTokenRequest() { + return getAccessTokenRequest(CoAP.Code.GET, CoapTransportResourceTest.ATTRIBUTES, null, CoapTransportResourceTest.GET_ATTRIBUTES_URI_QUERY); + } + + private static Request toRpcResponseAccessTokenRequest() { + return getAccessTokenRequest(CoAP.Code.POST, CoapTransportResourceTest.RPC, RANDOM.nextInt(100), null); + } + + private static Request toCertificateRequest(CoAP.Code method, String featureType) { + return getCertificateRequest(method, featureType, null, null); + } + + private static Request toGetAttributesCertificateRequest() { + return getCertificateRequest(CoAP.Code.GET, CoapTransportResourceTest.ATTRIBUTES, null, CoapTransportResourceTest.GET_ATTRIBUTES_URI_QUERY); + } + + private static Request toRpcResponseCertificateRequest() { + return getCertificateRequest(CoAP.Code.POST, CoapTransportResourceTest.RPC, RANDOM.nextInt(100), null); + } + + private static Request getAccessTokenRequest(CoAP.Code method, String featureType, Integer requestId, String uriQuery) { + return getRequest(method, featureType, false, requestId, uriQuery); + } + + private static Request getCertificateRequest(CoAP.Code method, String featureType, Integer requestId, String uriQuery) { + return getRequest(method, featureType, true, requestId, uriQuery); + } + + private static Request toProvisionRequest() { + return getRequest(CoAP.Code.POST, PROVISION, true, null, null); + } + + private static Request getRequest(CoAP.Code method, String featureType, boolean dtls, Integer requestId, String uriQuery) { + var request = new Request(method); + var options = new OptionSet(); + options.addUriPath(API); + options.addUriPath(V1); + if (!dtls) { + options.addUriPath(StringUtils.randomAlphanumeric(20)); + } + options.addUriPath(featureType); + if (requestId != null) { + options.addUriPath(String.valueOf(requestId)); + } + if (uriQuery != null) { + options.setUriQuery(uriQuery); + } + request.setOptions(options); + return request; + } + +} diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 09f822959f..8cf3e0efe8 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index f5ee1eea52..4433eae2b9 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java index 302bd20c8b..eef9a53024 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java @@ -52,7 +52,7 @@ import java.util.stream.Collectors; public class LwM2MModelConfigServiceImpl implements LwM2MModelConfigService { @Autowired - private TbLwM2MModelConfigStore modelStore; + TbLwM2MModelConfigStore modelStore; @Autowired @Lazy @@ -67,14 +67,14 @@ public class LwM2MModelConfigServiceImpl implements LwM2MModelConfigService { @Autowired private LwM2MTelemetryLogService logService; - private ConcurrentMap currentModelConfigs; + ConcurrentMap currentModelConfigs; @AfterStartUp(order = AfterStartUp.BEFORE_TRANSPORT_SERVICE) - private void init() { + public void init() { List models = modelStore.getAll(); log.debug("Fetched model configs: {}", models); currentModelConfigs = models.stream() - .collect(Collectors.toConcurrentMap(LwM2MModelConfig::getEndpoint, m -> m)); + .collect(Collectors.toConcurrentMap(LwM2MModelConfig::getEndpoint, m -> m, (existing, replacement) -> existing)); } @Override diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImplTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImplTest.java new file mode 100644 index 0000000000..fc54ca9e0b --- /dev/null +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImplTest.java @@ -0,0 +1,73 @@ +/** + * Copyright © 2016-2023 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.transport.lwm2m.server.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MModelConfigStore; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; + +class LwM2MModelConfigServiceImplTest { + + LwM2MModelConfigServiceImpl service; + TbLwM2MModelConfigStore modelStore; + + @BeforeEach + void setUp() { + service = new LwM2MModelConfigServiceImpl(); + modelStore = mock(TbLwM2MModelConfigStore.class); + service.modelStore = modelStore; + } + + @Test + void testInitWithDuplicatedModels() { + LwM2MModelConfig config = new LwM2MModelConfig("urn:imei:951358811362976"); + List models = List.of(config, config); + willReturn(models).given(modelStore).getAll(); + service.init(); + assertThat(service.currentModelConfigs).containsExactlyEntriesOf(Map.of(config.getEndpoint(), config)); + } + + @Test + void testInitWithNonUniqueEndpoints() { + LwM2MModelConfig configAlfa = new LwM2MModelConfig("urn:imei:951358811362976"); + LwM2MModelConfig configBravo = new LwM2MModelConfig("urn:imei:151358811362976"); + LwM2MModelConfig configDelta = new LwM2MModelConfig("urn:imei:151358811362976"); + assertThat(configBravo.getEndpoint()).as("non-unique endpoints provided").isEqualTo(configDelta.getEndpoint()); + List models = List.of(configAlfa, configBravo, configDelta); + willReturn(models).given(modelStore).getAll(); + service.init(); + assertThat(service.currentModelConfigs).containsExactlyInAnyOrderEntriesOf(Map.of( + configAlfa.getEndpoint(), configAlfa, + configBravo.getEndpoint(), configBravo + )); + } + + @Test + void testInitWithEmptyModels() { + willReturn(Collections.emptyList()).given(modelStore).getAll(); + service.init(); + assertThat(service.currentModelConfigs).isEmpty(); + } + +} diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index e11f923a0b..bb4767640c 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 6267ae9424..3bafbb518e 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -59,6 +59,7 @@ import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.msg.EncryptionUtil; +import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportService; @@ -136,6 +137,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private static final MqttQoS MAX_SUPPORTED_QOS_LVL = AT_LEAST_ONCE; private final UUID sessionId; + protected final MqttTransportContext context; private final TransportService transportService; private final SchedulerComponent scheduler; @@ -442,10 +444,12 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement MqttTransportAdaptor payloadAdaptor = deviceSessionCtx.getPayloadAdaptor(); if (deviceSessionCtx.isDeviceAttributesTopic(topicName)) { TransportProtos.PostAttributeMsg postAttributeMsg = payloadAdaptor.convertToPostAttributes(deviceSessionCtx, mqttMsg); - transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); + transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getMetadata(deviceSessionCtx, topicName), + getPubAckCallback(ctx, msgId, postAttributeMsg)); } else if (deviceSessionCtx.isDeviceTelemetryTopic(topicName)) { TransportProtos.PostTelemetryMsg postTelemetryMsg = payloadAdaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg); - transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); + transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getMetadata(deviceSessionCtx, topicName), + getPubAckCallback(ctx, msgId, postTelemetryMsg)); } else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX)) { TransportProtos.GetAttributeRequestMsg getAttributeMsg = payloadAdaptor.convertToGetAttributes(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); @@ -466,22 +470,28 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement getOtaPackageCallback(ctx, mqttMsg, msgId, fwMatcher, OtaPackageType.SOFTWARE); } else if (topicName.equals(MqttTopics.DEVICE_TELEMETRY_SHORT_TOPIC)) { TransportProtos.PostTelemetryMsg postTelemetryMsg = payloadAdaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg); - transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); + transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getMetadata(deviceSessionCtx, topicName), + getPubAckCallback(ctx, msgId, postTelemetryMsg)); } else if (topicName.equals(MqttTopics.DEVICE_TELEMETRY_SHORT_JSON_TOPIC)) { TransportProtos.PostTelemetryMsg postTelemetryMsg = context.getJsonMqttAdaptor().convertToPostTelemetry(deviceSessionCtx, mqttMsg); - transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); + transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getMetadata(deviceSessionCtx, topicName), + getPubAckCallback(ctx, msgId, postTelemetryMsg)); } else if (topicName.equals(MqttTopics.DEVICE_TELEMETRY_SHORT_PROTO_TOPIC)) { TransportProtos.PostTelemetryMsg postTelemetryMsg = context.getProtoMqttAdaptor().convertToPostTelemetry(deviceSessionCtx, mqttMsg); - transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); + transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getMetadata(deviceSessionCtx, topicName), + getPubAckCallback(ctx, msgId, postTelemetryMsg)); } else if (topicName.equals(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC)) { TransportProtos.PostAttributeMsg postAttributeMsg = payloadAdaptor.convertToPostAttributes(deviceSessionCtx, mqttMsg); - transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); + transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getMetadata(deviceSessionCtx, topicName), + getPubAckCallback(ctx, msgId, postAttributeMsg)); } else if (topicName.equals(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC)) { TransportProtos.PostAttributeMsg postAttributeMsg = context.getJsonMqttAdaptor().convertToPostAttributes(deviceSessionCtx, mqttMsg); - transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); + transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getMetadata(deviceSessionCtx, topicName), + getPubAckCallback(ctx, msgId, postAttributeMsg)); } else if (topicName.equals(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC)) { TransportProtos.PostAttributeMsg postAttributeMsg = context.getProtoMqttAdaptor().convertToPostAttributes(deviceSessionCtx, mqttMsg); - transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); + transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getMetadata(deviceSessionCtx, topicName), + getPubAckCallback(ctx, msgId, postAttributeMsg)); } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_RESPONSE_SHORT_JSON_TOPIC)) { TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = context.getJsonMqttAdaptor().convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_RESPONSE_SHORT_JSON_TOPIC); transportService.process(deviceSessionCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg)); @@ -525,6 +535,16 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + private TbMsgMetaData getMetadata(DeviceSessionCtx ctx, String topicName) { + if (ctx.isDeviceProfileMqttTransportType()) { + TbMsgMetaData md = new TbMsgMetaData(); + md.putValue(DataConstants.MQTT_TOPIC, topicName); + return md; + } else { + return null; + } + } + private void sendAckOrCloseSession(ChannelHandlerContext ctx, String topicName, int msgId) { if ((deviceSessionCtx.isSendAckOnValidationException() || MqttVersion.MQTT_5.equals(deviceSessionCtx.getMqttVersion())) && msgId > 0) { log.debug("[{}] Send pub ack on invalid publish msg [{}][{}]", sessionId, topicName, msgId); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index 95603e751c..5cd6ba9145 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -71,6 +71,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; +import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; @@ -85,7 +86,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMess @Slf4j public abstract class AbstractGatewaySessionHandler { - protected static final String DEFAULT_DEVICE_TYPE = "default"; private static final String CAN_T_PARSE_VALUE = "Can't parse value: "; private static final String DEVICE_PROPERTY = "device"; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java index 3816556732..44eceb8179 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java @@ -92,10 +92,14 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { private volatile boolean useJsonPayloadFormatForDefaultDownlinkTopics; private volatile boolean sendAckOnValidationException; + @Getter + private volatile boolean deviceProfileMqttTransportType; + @Getter @Setter private TransportPayloadType provisionPayloadType = payloadType; + public DeviceSessionCtx(UUID sessionId, ConcurrentMap mqttQoSMap, MqttTransportContext context) { super(sessionId, mqttQoSMap); this.context = context; @@ -165,6 +169,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { MqttDeviceProfileTransportConfiguration mqttConfig = (MqttDeviceProfileTransportConfiguration) transportConfiguration; TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttConfig.getTransportPayloadTypeConfiguration(); payloadType = transportPayloadTypeConfiguration.getTransportPayloadType(); + deviceProfileMqttTransportType = true; telemetryTopicFilter = MqttTopicFilterFactory.toFilter(mqttConfig.getDeviceTelemetryTopic()); attributesPublishTopicFilter = MqttTopicFilterFactory.toFilter(mqttConfig.getDeviceAttributesTopic()); attributesSubscribeTopicFilter = MqttTopicFilterFactory.toFilter(mqttConfig.getDeviceAttributesSubscribeTopic()); @@ -179,6 +184,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter(); attributesPublishTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter(); payloadType = TransportPayloadType.JSON; + deviceProfileMqttTransportType = false; sendAckOnValidationException = false; } updateAdaptor(); diff --git a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttTransportHandlerTest.java b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttTransportHandlerTest.java index 4892acb161..f2ee512760 100644 --- a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttTransportHandlerTest.java +++ b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttTransportHandlerTest.java @@ -16,8 +16,7 @@ package org.thingsboard.server.transport.mqtt; import io.netty.buffer.ByteBuf; -import io.netty.buffer.EmptyByteBuf; -import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.mqtt.MqttConnectMessage; import io.netty.handler.codec.mqtt.MqttConnectPayload; @@ -34,8 +33,19 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.JsonTransportPayloadConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; @@ -55,12 +65,14 @@ import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.willDoNothing; import static org.mockito.BDDMockito.willReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @Slf4j @RunWith(MockitoJUnitRunner.class) @@ -81,9 +93,14 @@ public class MqttTransportHandlerTest { ExecutorService executor; MqttTransportHandler handler; + @Spy + TransportService transportService; + @Before public void setUp() throws Exception { + willReturn(MSG_QUEUE_LIMIT).given(context).getMessageQueueSizePerDeviceLimit(); + willReturn(transportService).given(context).getTransportService(); handler = spy(new MqttTransportHandler(context, sslHandler)); willReturn(IP_ADDR).given(handler).getAddress(any()); @@ -104,9 +121,17 @@ public class MqttTransportHandlerTest { } MqttPublishMessage getMqttPublishMessage() { + return getMqttPublishMessage("v1/gateway/telemetry"); + } + + MqttPublishMessage getDeviceMqttPublishMessage() { + return getMqttPublishMessage("v1/devices/me/telemetry"); + } + + MqttPublishMessage getMqttPublishMessage(String topicName) { MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, true, MqttQoS.AT_LEAST_ONCE, false, 123); - MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader("v1/gateway/telemetry", packedId.incrementAndGet()); - ByteBuf payload = new EmptyByteBuf(new PooledByteBufAllocator()); + MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader(topicName, packedId.incrementAndGet()); + ByteBuf payload = Unpooled.wrappedBuffer("{\"testKey\":\"testValue\"}".getBytes()); return new MqttPublishMessage(mqttFixedHeader, variableHeader, payload); } @@ -205,4 +230,26 @@ public class MqttTransportHandlerTest { messages.forEach((msg) -> verify(handler, times(1)).processRegularSessionMsg(ctx, msg)); } + @Test + public void givenMqttMessage_whenDeviceProfileMqttTransport_thenTopicAddedToMetadata() { + MqttPublishMessage message = getDeviceMqttPublishMessage(); + when(context.getJsonMqttAdaptor()).thenReturn(new JsonMqttAdaptor()); + handler.deviceSessionCtx.setConnected(true); + DeviceProfile deviceProfile = new DeviceProfile(); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = new MqttDeviceProfileTransportConfiguration(); + mqttDeviceProfileTransportConfiguration.setTransportPayloadTypeConfiguration(new JsonTransportPayloadConfiguration()); + deviceProfileData.setTransportConfiguration(mqttDeviceProfileTransportConfiguration); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setTransportType(DeviceTransportType.MQTT); + handler.deviceSessionCtx.setDeviceProfile(deviceProfile); + + handler.processRegularSessionMsg(ctx, message); + + TbMsgMetaData expectedMd = new TbMsgMetaData(); + expectedMd.putValue(DataConstants.MQTT_TOPIC, message.variableHeader().topicName()); + + verify(transportService, times(1)).process(any(), (TransportProtos.PostTelemetryMsg) any(), eq(expectedMd), any()); + } + } \ No newline at end of file diff --git a/common/transport/pom.xml b/common/transport/pom.xml index 4fc43aa3ea..6879a4924d 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index 0a615cfd4f..1865f498fa 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 6840a24ab2..2e2174e0a9 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index cd61d42016..ceef62368a 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.transport; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.rpc.RpcStatus; +import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.common.transport.service.SessionMetaData; @@ -109,8 +110,12 @@ public interface TransportService { void process(SessionInfoProto sessionInfo, PostTelemetryMsg msg, TransportServiceCallback callback); + void process(SessionInfoProto sessionInfo, PostTelemetryMsg msg, TbMsgMetaData md, TransportServiceCallback callback); + void process(SessionInfoProto sessionInfo, PostAttributeMsg msg, TransportServiceCallback callback); + void process(SessionInfoProto sessionInfo, PostAttributeMsg msg, TbMsgMetaData md, TransportServiceCallback callback); + void process(SessionInfoProto sessionInfo, GetAttributeRequestMsg msg, TransportServiceCallback callback); void process(SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg msg, TransportServiceCallback callback); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 3b827edbae..48af9b9195 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -49,14 +49,14 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.limit.LimitedApi; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger; import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; -import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.common.stats.MessagesStats; import org.thingsboard.server.common.stats.StatsFactory; @@ -567,6 +567,11 @@ public class DefaultTransportService implements TransportService { @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostTelemetryMsg msg, TransportServiceCallback callback) { + process(sessionInfo, msg, null, callback); + } + + @Override + public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostTelemetryMsg msg, TbMsgMetaData md, TransportServiceCallback callback) { int dataPoints = 0; for (TransportProtos.TsKvListProto tsKv : msg.getTsKvListList()) { dataPoints += tsKv.getKvCount(); @@ -578,24 +583,29 @@ public class DefaultTransportService implements TransportService { CustomerId customerId = getCustomerId(sessionInfo); MsgPackCallback packCallback = new MsgPackCallback(msg.getTsKvListCount(), new ApiStatsProxyCallback<>(tenantId, customerId, dataPoints, callback)); for (TransportProtos.TsKvListProto tsKv : msg.getTsKvListList()) { - TbMsgMetaData metaData = new TbMsgMetaData(); + TbMsgMetaData metaData = md != null ? md.copy() : new TbMsgMetaData(); metaData.putValue("deviceName", sessionInfo.getDeviceName()); metaData.putValue("deviceType", sessionInfo.getDeviceType()); metaData.putValue("ts", tsKv.getTs() + ""); JsonObject json = JsonUtils.getJsonObject(tsKv.getKvList()); - sendToRuleEngine(tenantId, deviceId, customerId, sessionInfo, json, metaData, SessionMsgType.POST_TELEMETRY_REQUEST, packCallback); + sendToRuleEngine(tenantId, deviceId, customerId, sessionInfo, json, metaData, TbMsgType.POST_TELEMETRY_REQUEST, packCallback); } } } @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostAttributeMsg msg, TransportServiceCallback callback) { + process(sessionInfo, msg, null, callback); + } + + @Override + public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostAttributeMsg msg, TbMsgMetaData md, TransportServiceCallback callback) { if (checkLimits(sessionInfo, msg, callback, msg.getKvCount())) { reportActivityInternal(sessionInfo); TenantId tenantId = getTenantId(sessionInfo); DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); - TbMsgMetaData metaData = new TbMsgMetaData(); + TbMsgMetaData metaData = md != null ? md.copy() : new TbMsgMetaData(); metaData.putValue("deviceName", sessionInfo.getDeviceName()); metaData.putValue("deviceType", sessionInfo.getDeviceType()); if (msg.getShared()) { @@ -603,7 +613,7 @@ public class DefaultTransportService implements TransportService { } metaData.putValue(DataConstants.NOTIFY_DEVICE_METADATA_KEY, "false"); CustomerId customerId = getCustomerId(sessionInfo); - sendToRuleEngine(tenantId, deviceId, customerId, sessionInfo, json, metaData, SessionMsgType.POST_ATTRIBUTES_REQUEST, + sendToRuleEngine(tenantId, deviceId, customerId, sessionInfo, json, metaData, TbMsgType.POST_ATTRIBUTES_REQUEST, new TransportTbQueueCallback(new ApiStatsProxyCallback<>(tenantId, customerId, msg.getKvList().size(), callback))); } } @@ -723,7 +733,7 @@ public class DefaultTransportService implements TransportService { metaData.putValue("serviceId", serviceInfoProvider.getServiceId()); metaData.putValue("sessionId", sessionId.toString()); sendToRuleEngine(tenantId, deviceId, getCustomerId(sessionInfo), sessionInfo, json, metaData, - SessionMsgType.TO_SERVER_RPC_REQUEST, new TransportTbQueueCallback(callback)); + TbMsgType.TO_SERVER_RPC_REQUEST, new TransportTbQueueCallback(callback)); String requestId = sessionId + "-" + msg.getRequestId(); toServerRpcPendingMap.put(requestId, new RpcRequestMetadata(sessionId, msg.getRequestId())); scheduler.schedule(() -> processTimeout(requestId), clientSideRpcTimeout, TimeUnit.MILLISECONDS); @@ -1136,7 +1146,7 @@ public class DefaultTransportService implements TransportService { } private void sendToRuleEngine(TenantId tenantId, DeviceId deviceId, CustomerId customerId, TransportProtos.SessionInfoProto sessionInfo, JsonObject json, - TbMsgMetaData metaData, SessionMsgType sessionMsgType, TbQueueCallback callback) { + TbMsgMetaData metaData, TbMsgType tbMsgType, TbQueueCallback callback) { DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); DeviceProfile deviceProfile = deviceProfileCache.get(deviceProfileId); RuleChainId ruleChainId; @@ -1151,7 +1161,7 @@ public class DefaultTransportService implements TransportService { queueName = deviceProfile.getDefaultQueueName(); } - TbMsg tbMsg = TbMsg.newMsg(queueName, sessionMsgType.name(), deviceId, customerId, metaData, gson.toJson(json), ruleChainId, null); + TbMsg tbMsg = TbMsg.newMsg(queueName, tbMsgType, deviceId, customerId, metaData, gson.toJson(json), ruleChainId, null); sendToRuleEngine(tenantId, tbMsg, callback); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportTenantProfileCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportTenantProfileCache.java index d73e445516..12f89f29e6 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportTenantProfileCache.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportTenantProfileCache.java @@ -82,6 +82,7 @@ public class DefaultTransportTenantProfileCache implements TransportTenantProfil if (profileOpt.isPresent()) { TenantProfile newProfile = profileOpt.get(); log.trace("[{}] put: {}", newProfile.getId(), newProfile); + profiles.put(newProfile.getId(), newProfile); Set affectedTenants = tenantProfileIds.get(newProfile.getId()); return new TenantProfileUpdateResult(newProfile, affectedTenants != null ? affectedTenants : Collections.emptySet()); } else { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java index c9f126b808..e1192391d5 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java @@ -29,7 +29,7 @@ import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; @ConditionalOnExpression("'${service.type:null}'=='tb-transport'") public class TransportTenantRoutingInfoService implements TenantRoutingInfoService { - private TransportTenantProfileCache tenantProfileCache; + private final TransportTenantProfileCache tenantProfileCache; public TransportTenantRoutingInfoService(TransportTenantProfileCache tenantProfileCache) { this.tenantProfileCache = tenantProfileCache; @@ -38,7 +38,7 @@ public class TransportTenantRoutingInfoService implements TenantRoutingInfoServi @Override public TenantRoutingInfo getRoutingInfo(TenantId tenantId) { TenantProfile profile = tenantProfileCache.get(tenantId); - return new TenantRoutingInfo(tenantId, profile.isIsolatedTbRuleEngine()); + return new TenantRoutingInfo(tenantId, profile.getId(), profile.isIsolatedTbRuleEngine()); } } diff --git a/common/util/pom.xml b/common/util/pom.xml index 8d7cad921e..14ae1748b2 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java b/common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java new file mode 100644 index 0000000000..2ec15f3724 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2023 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.common.util; + +import com.google.gson.JsonParseException; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.EntityId; + +import javax.script.ScriptException; +import java.io.PrintWriter; +import java.io.StringWriter; + +@Slf4j +public class ExceptionUtil { + + @SuppressWarnings("unchecked") + public static T lookupException(Throwable source, Class clazz) { + Exception e = lookupExceptionInCause(source, clazz); + if (e != null) { + return (T) e; + } else { + return null; + } + } + + public static Exception lookupExceptionInCause(Throwable source, Class... clazzes) { + while (source != null) { + for (Class clazz : clazzes) { + if (clazz.isAssignableFrom(source.getClass())) { + return (Exception) source; + } + } + source = source.getCause(); + } + return null; + } + + public static String toString(Exception e, EntityId componentId, boolean stackTraceEnabled) { + Exception exception = lookupExceptionInCause(e, ScriptException.class, JsonParseException.class); + if (exception != null && StringUtils.isNotEmpty(exception.getMessage())) { + return exception.getMessage(); + } else { + if (stackTraceEnabled) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } else { + log.debug("[{}] Unknown error during message processing", componentId, e); + return "Please contact system administrator"; + } + } + } +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 0e4204fd95..9f980f36c8 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -44,6 +44,7 @@ import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.function.UnaryOperator; +import java.util.regex.Pattern; /** * Created by Valerii Sosliuk on 5/12/2017. @@ -227,7 +228,7 @@ public class JacksonUtil { return node; } - public static void replaceUuidsRecursively(JsonNode node, Set skipFieldsSet, UnaryOperator replacer) { + public static void replaceUuidsRecursively(JsonNode node, Set skipFieldsSet, Pattern includedFieldsPattern, UnaryOperator replacer) { if (node == null) { return; } @@ -239,9 +240,14 @@ public class JacksonUtil { if (skipFieldsSet.contains(fieldName)) { continue; } + if (includedFieldsPattern != null) { + if (!RegexUtils.matches(fieldName, includedFieldsPattern)) { + continue; + } + } var child = objectNode.get(fieldName); if (child.isObject() || child.isArray()) { - replaceUuidsRecursively(child, skipFieldsSet, replacer); + replaceUuidsRecursively(child, skipFieldsSet, includedFieldsPattern, replacer); } else if (child.isTextual()) { String text = child.asText(); String newText = RegexUtils.replace(text, RegexUtils.UUID_PATTERN, uuid -> replacer.apply(UUID.fromString(uuid)).toString()); @@ -255,7 +261,7 @@ public class JacksonUtil { for (int i = 0; i < array.size(); i++) { JsonNode arrayElement = array.get(i); if (arrayElement.isObject() || arrayElement.isArray()) { - replaceUuidsRecursively(arrayElement, skipFieldsSet, replacer); + replaceUuidsRecursively(arrayElement, skipFieldsSet, includedFieldsPattern, replacer); } else if (arrayElement.isTextual()) { String text = arrayElement.asText(); String newText = RegexUtils.replace(text, RegexUtils.UUID_PATTERN, uuid -> replacer.apply(UUID.fromString(uuid)).toString()); diff --git a/common/util/src/test/java/org/thingsboard/common/util/ExceptionUtilTest.java b/common/util/src/test/java/org/thingsboard/common/util/ExceptionUtilTest.java new file mode 100644 index 0000000000..e589ae8e30 --- /dev/null +++ b/common/util/src/test/java/org/thingsboard/common/util/ExceptionUtilTest.java @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2023 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.common.util; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +class ExceptionUtilTest { + + final Exception cause = new RuntimeException(); + + @Test + void givenRootCause_whenLookupExceptionInCause_thenReturnRootCauseAndNoStackOverflow() { + Exception e = cause; + for (int i = 0; i <= 16384; i++) { + e = new Exception(e); + } + assertThat(ExceptionUtil.lookupExceptionInCause(e, RuntimeException.class)).isSameAs(cause); + } + + @Test + void givenCause_whenLookupExceptionInCause_thenReturnCause() { + assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(cause), RuntimeException.class)).isSameAs(cause); + } + + @Test + void givenNoCauseAndExceptionIsWantedCauseClass_whenLookupExceptionInCause_thenReturnSelf() { + assertThat(ExceptionUtil.lookupExceptionInCause(cause, RuntimeException.class)).isSameAs(cause); + } + + @Test + void givenNoCause_whenLookupExceptionInCause_thenReturnNull() { + assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(), RuntimeException.class)).isNull(); + } + + @Test + void givenNotWantedCause_whenLookupExceptionInCause_thenReturnNull() { + final Exception cause = new IOException(); + assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(cause), RuntimeException.class)).isNull(); + } + + @Test + void givenCause_whenLookupExceptionInCauseByMany_thenReturnFirstCause() { + final Exception causeIAE = new IllegalAccessException(); + assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIAE))).isNull(); + assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIAE), IOException.class, NoSuchFieldException.class)).isNull(); + assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIAE), IllegalAccessException.class, IOException.class, NoSuchFieldException.class)).isSameAs(causeIAE); + assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIAE), IOException.class, NoSuchFieldException.class, IllegalAccessException.class)).isSameAs(causeIAE); + + final Exception causeIOE = new IOException(causeIAE); + assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIOE))).isNull(); + assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIAE), ClassNotFoundException.class, NoSuchFieldException.class)).isNull(); + assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIOE), IOException.class, NoSuchFieldException.class)).isSameAs(causeIOE); + assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIOE), IllegalAccessException.class, IOException.class, NoSuchFieldException.class)).isSameAs(causeIOE); + assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIOE), IOException.class, NoSuchFieldException.class, IllegalAccessException.class)).isSameAs(causeIOE); + } + +} diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index ae12a413c0..a8acdf2be9 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT common org.thingsboard.common diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java index 618eb11d98..ab2dc417ca 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java @@ -122,7 +122,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe @Value("${queue.vc.poll-interval:25}") private long pollDuration; - @Value("${queue.vc.pack-processing-timeout:60000}") + @Value("${queue.vc.pack-processing-timeout:180000}") private long packProcessingTimeout; @Value("${vc.git.io_pool_size:3}") private int ioPoolSize; diff --git a/dao/pom.xml b/dao/pom.xml index ece2215327..420c72bd7d 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT thingsboard dao @@ -79,6 +79,10 @@ org.postgresql postgresql + + org.bouncycastle + bcpkix-jdk15on + org.springframework.boot spring-boot-starter-test diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 63b13bca66..76f5a1948b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -18,6 +18,10 @@ package org.thingsboard.server.dao; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.util.CollectionUtils; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -33,6 +37,7 @@ import java.util.Optional; import java.util.UUID; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Collectors; public abstract class DaoUtil { @@ -109,6 +114,10 @@ public abstract class DaoUtil { return ids; } + public static List fromUUIDs(List uuids, Function mapper) { + return uuids.stream().map(mapper).collect(Collectors.toList()); + } + public static I toEntityId(UUID uuid, Function creator) { if (uuid != null) { return creator.apply(uuid); @@ -142,4 +151,16 @@ public abstract class DaoUtil { return null; } } + + public static List convertTenantEntityTypesToDto(UUID tenantId, EntityType entityType, List types) { + if (CollectionUtils.isEmpty(types)) { + return Collections.emptyList(); + } + + List list = new ArrayList<>(types.size()); + for (String type : types) { + list.add(new EntitySubtype(TenantId.fromUUID(tenantId), entityType, type)); + } + return list; + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index 745861c6c8..2b6594a831 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.alarm; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; @@ -77,6 +78,8 @@ public interface AlarmDao extends Dao { PageData findAlarmsIdsByEndTsBeforeAndTenantId(Long time, TenantId tenantId, PageLink pageLink); + PageData findAlarmIdsByAssigneeId(TenantId tenantId, UUID userId, PageLink pageLink); + void createEntityAlarmRecord(EntityAlarm entityAlarm); List findEntityAlarmRecords(TenantId tenantId, AlarmId id); @@ -97,4 +100,7 @@ public interface AlarmDao extends Dao { long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); + PageData findTenantAlarmTypes(UUID tenantId, PageLink pageLink); + + boolean removeAlarmTypesIfNoAlarmsPresent(UUID tenantId, Set types); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCacheEvictEvent.java new file mode 100644 index 0000000000..985d0c8ad1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCacheEvictEvent.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.alarm; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@RequiredArgsConstructor +class AlarmTypesCacheEvictEvent { + private final TenantId tenantId; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCaffeineCache.java new file mode 100644 index 0000000000..4ad08338c4 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCaffeineCache.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.alarm; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("AlarmTypesCache") +public class AlarmTypesCaffeineCache extends CaffeineTbTransactionalCache> { + + public AlarmTypesCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.ALARM_TYPES_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesRedisCache.java new file mode 100644 index 0000000000..18eb81f690 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesRedisCache.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.alarm; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbFSTRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("AlarmTypesCache") +public class AlarmTypesRedisCache extends RedisTbTransactionalCache> { + + public AlarmTypesRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.ALARM_TYPES_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 96342aca2f..33f6ac0265 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -24,7 +24,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.util.CollectionUtils; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; @@ -39,6 +41,7 @@ import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.EntityAlarm; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ApiUsageLimitsExceededException; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; @@ -47,6 +50,8 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -54,8 +59,11 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; -import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.DataValidator; @@ -78,19 +86,33 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Service("AlarmDaoService") @Slf4j @RequiredArgsConstructor -public class BaseAlarmService extends AbstractEntityService implements AlarmService { +public class BaseAlarmService extends AbstractCachedEntityService, AlarmTypesCacheEvictEvent> implements AlarmService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + private static final PageLink DEFAULT_ALARM_TYPES_PAGE_LINK = new PageLink(25, 0, null, new SortOrder("type")); + private final TenantService tenantService; private final AlarmDao alarmDao; private final EntityService entityService; private final DataValidator alarmDataValidator; + @TransactionalEventListener(classes = AlarmTypesCacheEvictEvent.class) + @Override + public void handleEvictEvent(AlarmTypesCacheEvictEvent event) { + TenantId tenantId = event.getTenantId(); + cache.evict(tenantId); + } + @Override public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { validateAlarmRequest(request); - return withPropagated(alarmDao.updateAlarm(request)); + AlarmApiCallResult result = withPropagated(alarmDao.updateAlarm(request)); + if (result.getAlarm() != null) { + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getAlarm().getTenantId()).entity(result) + .entityId(result.getAlarm().getId()).build()); + } + return result; } @Override @@ -112,17 +134,32 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ if (!result.isSuccessful() && !alarmCreationEnabled) { throw new ApiUsageLimitsExceededException("Alarms creation is disabled"); } + if (result.getAlarm() != null) { + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getAlarm().getTenantId()) + .entityId(result.getAlarm().getId()).added(true).build()); + publishEvictEvent(new AlarmTypesCacheEvictEvent(request.getTenantId())); + } return withPropagated(result); } @Override public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { - return withPropagated(alarmDao.acknowledgeAlarm(tenantId, alarmId, ackTs)); + var result = withPropagated(alarmDao.acknowledgeAlarm(tenantId, alarmId, ackTs)); + if (result.getAlarm() != null) { + eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entityId(result.getAlarm().getId()) + .actionType(ActionType.ALARM_ACK).build()); + } + return result; } @Override public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) { - return withPropagated(alarmDao.clearAlarm(tenantId, alarmId, clearTs, details)); + var result = withPropagated(alarmDao.clearAlarm(tenantId, alarmId, clearTs, details)); + if (result.getAlarm() != null) { + eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entityId(result.getAlarm().getId()) + .actionType(ActionType.ALARM_CLEAR).build()); + } + return result; } @Override @@ -181,6 +218,12 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ @Override @Transactional public AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId) { + return delAlarm(tenantId, alarmId, true); + } + + @Override + @Transactional + public AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId, boolean checkAndDeleteAlarmType) { log.debug("Deleting Alarm Id: {}", alarmId); AlarmInfo alarm = alarmDao.findAlarmInfoById(tenantId, alarmId.getId()); if (alarm == null) { @@ -188,10 +231,23 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } else { deleteEntityRelations(tenantId, alarm.getId()); alarmDao.removeById(tenantId, alarm.getUuidId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId) + .entityId(alarmId).entity(alarm).build()); + if (checkAndDeleteAlarmType) { + delAlarmTypes(tenantId, Collections.singleton(alarm.getType())); + } return AlarmApiCallResult.builder().alarm(alarm).deleted(true).successful(true).build(); } } + @Override + @Transactional + public void delAlarmTypes(TenantId tenantId, Set types) { + if (!types.isEmpty() && alarmDao.removeAlarmTypesIfNoAlarmsPresent(tenantId.getId(), types)) { + publishEvictEvent(new AlarmTypesCacheEvictEvent(tenantId)); + } + } + @Override @Transactional public AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId) { @@ -300,12 +356,22 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ @Override public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTime) { - return withPropagated(alarmDao.assignAlarm(tenantId, alarmId, assigneeId, assignTime)); + var result = withPropagated(alarmDao.assignAlarm(tenantId, alarmId, assigneeId, assignTime)); + if (result.getAlarm() != null) { + eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entityId(result.getAlarm().getId()) + .actionType(ActionType.ALARM_ASSIGNED).build()); + } + return result; } @Override public AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime) { - return withPropagated(alarmDao.unassignAlarm(tenantId, alarmId, unassignTime)); + var result = withPropagated(alarmDao.unassignAlarm(tenantId, alarmId, unassignTime)); + if (result.getAlarm() != null) { + eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entityId(result.getAlarm().getId()) + .actionType(ActionType.ALARM_UNASSIGNED).build()); + } + return result; } @Override @@ -349,6 +415,13 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return Futures.immediateFuture(alarmDao.findCustomerAlarmsV2(tenantId, customerId, query)); } + @Override + public PageData findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, PageLink pageLink) { + log.trace("[{}] Executing findAlarmIdsByAssigneeId [{}]", tenantId, userId); + validateId(userId, "Incorrect userId " + userId); + return alarmDao.findAlarmIdsByAssigneeId(tenantId, userId.getId(), pageLink); + } + @Override public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) { @@ -376,6 +449,17 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return alarmDao.countAlarmsByQuery(tenantId, customerId, query); } + @Override + public PageData findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findAlarmTypesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + if (DEFAULT_ALARM_TYPES_PAGE_LINK.equals(pageLink)) { + return cache.getAndPutInTransaction(tenantId, () -> + alarmDao.findTenantAlarmTypes(tenantId.getId(), pageLink), false); + } + return alarmDao.findTenantAlarmTypes(tenantId.getId(), pageLink); + } + private Alarm merge(Alarm existing, Alarm alarm) { if (alarm.getStartTs() > existing.getEndTs()) { existing.setEndTs(alarm.getStartTs()); @@ -487,5 +571,4 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ request.setEndTs(request.getStartTs()); } } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index eb297e0049..9240bbc223 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -33,6 +33,8 @@ 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.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -109,15 +111,31 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService kvEntries) { - kvEntries.forEach(AttributeUtils::validate); + public static void validate(List kvEntries, boolean valueNoXssValidation) { + kvEntries.forEach(tsKvEntry -> validate(tsKvEntry, valueNoXssValidation)); } - public static void validate(AttributeKvEntry kvEntry) { - KvUtils.validate(kvEntry); + public static void validate(AttributeKvEntry kvEntry, boolean valueNoXssValidation) { + KvUtils.validate(kvEntry, valueNoXssValidation); if (kvEntry.getDataType() == null) { throw new IncorrectParameterException("Incorrect kvEntry. Data type can't be null"); } else { diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 09414ac750..f855c116e2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.attributes; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; @@ -45,6 +46,9 @@ import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; public class BaseAttributesService implements AttributesService { private final AttributesDao attributesDao; + @Value("${sql.attributes.value_no_xss_validation:false}") + private boolean valueNoXssValidation; + public BaseAttributesService(AttributesDao attributesDao) { this.attributesDao = attributesDao; } @@ -82,14 +86,14 @@ public class BaseAttributesService implements AttributesService { @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) { validate(entityId, scope); - AttributeUtils.validate(attribute); + AttributeUtils.validate(attribute, valueNoXssValidation); return attributesDao.save(tenantId, entityId, scope, attribute); } @Override public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { validate(entityId, scope); - AttributeUtils.validate(attributes); + AttributeUtils.validate(attributes, valueNoXssValidation); List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); return Futures.allAsList(saveFutures); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index b95ce39d9a..faff81670b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -69,6 +69,8 @@ public class CachedAttributesService implements AttributesService { @Value("${cache.type:caffeine}") private String cacheType; + @Value("${sql.attributes.value_no_xss_validation:false}") + private boolean valueNoXssValidation; public CachedAttributesService(AttributesDao attributesDao, StatsFactory statsFactory, @@ -212,7 +214,7 @@ public class CachedAttributesService implements AttributesService { @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) { validate(entityId, scope); - AttributeUtils.validate(attribute); + AttributeUtils.validate(attribute, valueNoXssValidation); ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); return Futures.transform(future, key -> evict(entityId, scope, attribute, key), cacheExecutor); } @@ -220,7 +222,7 @@ public class CachedAttributesService implements AttributesService { @Override public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { validate(entityId, scope); - AttributeUtils.validate(attributes); + AttributeUtils.validate(attributes, valueNoXssValidation); List> futures = new ArrayList<>(attributes.size()); for (var attribute : attributes) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 1d8704f0e6..e69ec5865c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.customer; -import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -36,6 +35,8 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityCountService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -43,7 +44,6 @@ import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; -import java.io.IOException; import java.util.Optional; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -112,6 +112,8 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom if (customer.getId() == null) { countService.publishCountEntityEvictEvent(savedCustomer.getTenantId(), EntityType.CUSTOMER); } + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCustomer.getTenantId()) + .entityId(savedCustomer.getId()).added(customer.getId() == null).build()); return savedCustomer; } catch (Exception e) { checkConstraintViolation(e, "customer_external_id_unq_key", "Customer with such external id already exists!"); @@ -139,6 +141,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom apiUsageStateService.deleteApiUsageStateByEntityId(customerId); customerDao.removeById(tenantId, customerId.getId()); countService.publishCountEntityEvictEvent(tenantId, EntityType.CUSTOMER); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(customerId).build()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 31fc29dd69..c74aba5d5c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; @@ -45,6 +46,9 @@ import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.edge.EdgeDao; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityCountService; +import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -133,13 +137,26 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb return dashboardInfoDao.findByIdAsync(tenantId, dashboardId.getId()); } + @Override + public Dashboard saveDashboard(Dashboard dashboard, boolean doValidate) { + return doSaveDashboard(dashboard, doValidate); + } + @Override public Dashboard saveDashboard(Dashboard dashboard) { + return doSaveDashboard(dashboard, true); + } + + private Dashboard doSaveDashboard(Dashboard dashboard, boolean doValidate) { log.trace("Executing saveDashboard [{}]", dashboard); - dashboardValidator.validate(dashboard, DashboardInfo::getTenantId); + if (doValidate) { + dashboardValidator.validate(dashboard, DashboardInfo::getTenantId); + } try { var saved = dashboardDao.save(dashboard.getTenantId(), dashboard); publishEvictEvent(new DashboardTitleEvictEvent(saved.getId())); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(saved.getTenantId()) + .entityId(saved.getId()).added(dashboard.getId() == null).build()); if (dashboard.getId() == null) { countService.publishCountEntityEvictEvent(saved.getTenantId(), EntityType.DASHBOARD); } @@ -215,6 +232,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb dashboardDao.removeById(tenantId, dashboardId.getId()); publishEvictEvent(new DashboardTitleEvictEvent(dashboardId)); countService.publishCountEntityEvictEvent(tenantId, EntityType.DASHBOARD); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(dashboardId).build()); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_dashboard_device_profile")) { @@ -304,6 +322,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb log.warn("[{}] Failed to create dashboard relation. Edge Id: [{}]", dashboardId, edgeId); throw new RuntimeException(e); } + eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).edgeId(edgeId).entityId(dashboardId) + .actionType(ActionType.ASSIGNED_TO_EDGE).build()); return dashboard; } @@ -320,6 +340,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb log.warn("[{}] Failed to delete dashboard relation. Edge Id: [{}]", dashboardId, edgeId); throw new RuntimeException(e); } + eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).edgeId(edgeId).entityId(dashboardId) + .actionType(ActionType.UNASSIGNED_FROM_EDGE).build()); return dashboard; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java new file mode 100644 index 0000000000..227b531be0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@ConfigurationProperties(prefix = "device") +@Data +public class DeviceConnectivityConfiguration { + private Map connectivity = new HashMap<>(); + + public DeviceConnectivityInfo getConnectivity(String protocol) { + return connectivity.get(protocol); + } + + public boolean isEnabled(String protocol) { + var info = connectivity.get(protocol); + return info != null && info.isEnabled(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityInfo.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityInfo.java new file mode 100644 index 0000000000..b243be9995 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityInfo.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import lombok.Data; + +@Data +public class DeviceConnectivityInfo { + private boolean enabled; + private String host; + private String port; + private String pemCertFile; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java new file mode 100644 index 0000000000..b30ae40808 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java @@ -0,0 +1,329 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.openssl.PEMParser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.ResourceUtils; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.dao.util.DeviceConnectivityUtil; + +import javax.annotation.PostConstruct; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.CHECK_DOCUMENTATION; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS; + +@Service("DeviceConnectivityDaoService") +@Slf4j +public class DeviceConnectivityServiceImpl implements DeviceConnectivityService { + + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId "; + public static final String DEFAULT_DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry"; + + private final Map certs = new ConcurrentHashMap<>(); + + @Autowired + private DeviceCredentialsService deviceCredentialsService; + + @Autowired + private DeviceProfileService deviceProfileService; + + @Autowired + private DeviceConnectivityConfiguration deviceConnectivityConfiguration; + + @PostConstruct + private void init() { + DeviceConnectivityInfo mqtts = deviceConnectivityConfiguration.getConnectivity(MQTTS); + if (mqtts != null && mqtts.isEnabled()) { + String certFilePath = mqtts.getPemCertFile(); + if (StringUtils.isBlank(certFilePath) || !ResourceUtils.resourceExists(this, certFilePath)) { + String error = StringUtils.isBlank(certFilePath) ? "path is empty" : "file is not exists"; + log.error("MQTTS is enabled but cert {}!", error); + } + } + } + + @Override + public JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException { + DeviceId deviceId = device.getId(); + log.trace("Executing findDevicePublishTelemetryCommands [{}]", deviceId); + validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); + + DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), deviceId); + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId()); + DeviceTransportType transportType = deviceProfile.getTransportType(); + + ObjectNode commands = JacksonUtil.newObjectNode(); + switch (transportType) { + case DEFAULT: + Optional.ofNullable(getHttpTransportPublishCommands(baseUrl, creds)) + .ifPresent(v -> commands.set(HTTP, v)); + Optional.ofNullable(getMqttTransportPublishCommands(baseUrl, creds)) + .ifPresent(v -> commands.set(MQTT, v)); + Optional.ofNullable(getCoapTransportPublishCommands(baseUrl, creds)) + .ifPresent(v -> commands.set(COAP, v)); + break; + case MQTT: + MqttDeviceProfileTransportConfiguration transportConfiguration = + (MqttDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration(); + //TODO: add sparkplug command with emulator (check SSL) + if (transportConfiguration.isSparkplug()) { + ObjectNode sparkplug = JacksonUtil.newObjectNode(); + sparkplug.put("sparkplug", CHECK_DOCUMENTATION); + commands.set(MQTT, sparkplug); + } else { + String topicName = transportConfiguration.getDeviceTelemetryTopic(); + + Optional.ofNullable(getMqttTransportPublishCommands(baseUrl, topicName, creds)) + .ifPresent(v -> commands.set(MQTT, v)); + } + break; + case COAP: + Optional.ofNullable(getCoapTransportPublishCommands(baseUrl, creds)) + .ifPresent(v -> commands.set(COAP, v)); + break; + default: + commands.put(transportType.name(), CHECK_DOCUMENTATION); + } + return commands; + } + + @Override + public Resource getPemCertFile(String protocol) { + return certs.computeIfAbsent(protocol, key -> { + DeviceConnectivityInfo connectivity = deviceConnectivityConfiguration.getConnectivity(protocol); + if (connectivity == null) { + log.warn("Unknown connectivity protocol: {}", protocol); + return null; + } + String certFilePath = connectivity.getPemCertFile(); + if (StringUtils.isNotBlank(certFilePath) && ResourceUtils.resourceExists(this, certFilePath)) { + try { + return getCert(certFilePath); + } catch (Exception e) { + String msg = String.format("Failed to read %s server certificate!", protocol); + log.warn(msg); + throw new RuntimeException(msg, e); + } + } else { + return null; + } + }); + } + + private Resource getCert(String path) throws Exception { + StringBuilder pemContentBuilder = new StringBuilder(); + + try (InputStream inStream = ResourceUtils.getInputStream(this, path); + PEMParser pemParser = new PEMParser(new InputStreamReader(inStream))) { + + Object object; + + while ((object = pemParser.readObject()) != null) { + if (object instanceof X509CertificateHolder) { + var certHolder = (X509CertificateHolder) object; + String certBase64 = Base64.getEncoder().encodeToString(certHolder.getEncoded()); + + pemContentBuilder.append("-----BEGIN CERTIFICATE-----\n"); + int index = 0; + while (index < certBase64.length()) { + pemContentBuilder.append(certBase64, index, Math.min(index + 64, certBase64.length())); + pemContentBuilder.append("\n"); + index += 64; + } + pemContentBuilder.append("-----END CERTIFICATE-----\n"); + } + } + } + + return new ByteArrayResource(pemContentBuilder.toString().getBytes(StandardCharsets.UTF_8)); + } + + private JsonNode getHttpTransportPublishCommands(String defaultHostname, DeviceCredentials deviceCredentials) throws URISyntaxException { + ObjectNode httpCommands = JacksonUtil.newObjectNode(); + Optional.ofNullable(getHttpPublishCommand(HTTP, defaultHostname, deviceCredentials)) + .ifPresent(v -> httpCommands.put(HTTP, v)); + Optional.ofNullable(getHttpPublishCommand(HTTPS, defaultHostname, deviceCredentials)) + .ifPresent(v -> httpCommands.put(HTTPS, v)); + return httpCommands.isEmpty() ? null : httpCommands; + } + + private String getHttpPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { + DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity(protocol); + if (properties == null || !properties.isEnabled() || + deviceCredentials.getCredentialsType() != DeviceCredentialsType.ACCESS_TOKEN) { + return null; + } + String hostName = getHost(baseUrl, properties); + String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort(); + + return DeviceConnectivityUtil.getHttpPublishCommand(protocol, hostName, port, deviceCredentials); + } + + private JsonNode getMqttTransportPublishCommands(String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { + return getMqttTransportPublishCommands(baseUrl, DEFAULT_DEVICE_TELEMETRY_TOPIC, deviceCredentials); + } + + private JsonNode getMqttTransportPublishCommands(String baseUrl, String topic, DeviceCredentials deviceCredentials) throws URISyntaxException { + ObjectNode mqttCommands = JacksonUtil.newObjectNode(); + + if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) { + mqttCommands.put(MQTTS, CHECK_DOCUMENTATION); + return mqttCommands; + } + + ObjectNode dockerMqttCommands = JacksonUtil.newObjectNode(); + + if (deviceConnectivityConfiguration.isEnabled(MQTT)) { + Optional.ofNullable(getMqttPublishCommand(baseUrl, topic, deviceCredentials)). + ifPresent(v -> mqttCommands.put(MQTT, v)); + + Optional.ofNullable(getDockerMqttPublishCommand(MQTT, baseUrl, topic, deviceCredentials)) + .ifPresent(v -> dockerMqttCommands.put(MQTT, v)); + } + + if (deviceConnectivityConfiguration.isEnabled(MQTTS)) { + List mqttsPublishCommand = getMqttsPublishCommand(baseUrl, topic, deviceCredentials); + if (mqttsPublishCommand != null) { + ArrayNode arrayNode = mqttCommands.putArray(MQTTS); + mqttsPublishCommand.forEach(arrayNode::add); + } + + Optional.ofNullable(getDockerMqttPublishCommand(MQTTS, baseUrl, topic, deviceCredentials)) + .ifPresent(v -> dockerMqttCommands.put(MQTTS, v)); + } + + if (!dockerMqttCommands.isEmpty()) { + mqttCommands.set(DOCKER, dockerMqttCommands); + } + return mqttCommands.isEmpty() ? null : mqttCommands; + } + + private String getMqttPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException { + DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity(MQTT); + String mqttHost = getHost(baseUrl, properties); + String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); + return DeviceConnectivityUtil.getMqttPublishCommand(MQTT, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials); + } + + private List getMqttsPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException { + DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity(MQTTS); + String mqttHost = getHost(baseUrl, properties); + String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); + String pubCommand = DeviceConnectivityUtil.getMqttPublishCommand(MQTTS, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials); + + ArrayList commands = new ArrayList<>(); + if (pubCommand != null) { + commands.add(DeviceConnectivityUtil.getCurlPemCertCommand(baseUrl, MQTTS)); + commands.add(pubCommand); + return commands; + } + return null; + } + + private String getDockerMqttPublishCommand(String protocol, String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException { + DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity(protocol); + String mqttHost = getHost(baseUrl, properties); + String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); + return DeviceConnectivityUtil.getDockerMqttPublishCommand(protocol, baseUrl, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials); + } + + private JsonNode getCoapTransportPublishCommands(String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { + ObjectNode coapCommands = JacksonUtil.newObjectNode(); + + if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) { + coapCommands.put(COAPS, CHECK_DOCUMENTATION); + return coapCommands; + } + + ObjectNode dockerCoapCommands = JacksonUtil.newObjectNode(); + + if (deviceConnectivityConfiguration.isEnabled(COAP)) { + Optional.ofNullable(getCoapPublishCommand(COAP, baseUrl, deviceCredentials)) + .ifPresent(v -> coapCommands.put(COAP, v)); + + Optional.ofNullable(getDockerCoapPublishCommand(COAP, baseUrl, deviceCredentials)) + .ifPresent(v -> dockerCoapCommands.put(COAP, v)); + } + + if (deviceConnectivityConfiguration.isEnabled(COAPS)) { + Optional.ofNullable(getCoapPublishCommand(COAPS, baseUrl, deviceCredentials)) + .ifPresent(v -> coapCommands.put(COAPS, v)); + + Optional.ofNullable(getDockerCoapPublishCommand(COAPS, baseUrl, deviceCredentials)) + .ifPresent(v -> dockerCoapCommands.put(COAPS, v)); + } + + if (!dockerCoapCommands.isEmpty()) { + coapCommands.set(DOCKER, dockerCoapCommands); + } + + return coapCommands.isEmpty() ? null : coapCommands; + } + + private String getCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { + DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity(protocol); + String hostName = getHost(baseUrl, properties); + String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort(); + return DeviceConnectivityUtil.getCoapPublishCommand(protocol, hostName, port, deviceCredentials); + } + + private String getDockerCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { + DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity(protocol); + String host = getHost(baseUrl, properties); + String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort(); + return DeviceConnectivityUtil.getDockerCoapPublishCommand(protocol, host, port, deviceCredentials); + } + + private String getHost(String baseUrl, DeviceConnectivityInfo properties) throws URISyntaxException { + return properties.getHost().isEmpty() ? new URI(baseUrl).getHost() : properties.getHost(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index 6e79b953ff..13865950ed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -25,6 +25,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredential; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; @@ -41,6 +42,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -107,6 +109,9 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService sync = new ThreadLocal<>(); + + @Override + public boolean isSync() { + Boolean sync = this.sync.get(); + return sync != null && sync; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java index 17d512a786..58d96fe33b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java @@ -170,4 +170,12 @@ public interface EdgeDao extends Dao { */ PageData findEdgesByTenantIdAndEntityId(UUID tenantId, UUID entityId, EntityType entityType, PageLink pageLink); -} \ No newline at end of file + /** + * Find edges by tenantProfileId. + * + * @param tenantProfileId the tenantProfileId + * @return the list of edge objects + */ + PageData findEdgesByTenantProfileId(UUID tenantProfileId, PageLink pageLink); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 31b446fbd5..4e7e3f0892 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -33,7 +33,9 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.edge.EdgeSearchQuery; @@ -44,8 +46,10 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -53,12 +57,14 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; import javax.annotation.Nullable; @@ -98,6 +104,9 @@ public class EdgeServiceImpl extends AbstractCachedEntityService edgeValidator; @@ -179,6 +188,8 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgesByTenantProfileId(TenantProfileId tenantProfileId, PageLink pageLink) { + log.trace("Executing findEdgesByTenantProfileId, tenantProfileId [{}], pageLink [{}]", tenantProfileId, pageLink); + Validator.validateId(tenantProfileId, "Incorrect tenantProfileId " + tenantProfileId); + validatePageLink(pageLink); + return edgeDao.findEdgesByTenantProfileId(tenantProfileId.getId(), pageLink); + } + private PaginatedRemover tenantEdgesRemover = new PaginatedRemover() { @@ -451,6 +474,8 @@ public class EdgeServiceImpl extends AbstractCachedEntityService cache; - @Autowired - private ApplicationEventPublisher eventPublisher; - protected void publishEvictEvent(E event) { if (TransactionSynchronizationManager.isActualTransactionActive()) { eventPublisher.publishEvent(event); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 6d4ac9f97b..17cd7a15d4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.entity; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.StringUtils; @@ -43,6 +44,9 @@ public abstract class AbstractEntityService { public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; + @Autowired + protected ApplicationEventPublisher eventPublisher; + @Lazy @Autowired protected RelationService relationService; @@ -113,7 +117,7 @@ public abstract class AbstractEntityService { List entityViews = entityViewService.findEntityViewsByTenantIdAndEntityId(tenantId, entityId); if (entityViews != null && !entityViews.isEmpty()) { EntityView entityView = entityViews.get(0); - Boolean relationExists = relationService.checkRelation( + boolean relationExists = relationService.checkRelation( tenantId, edgeId, entityView.getId(), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE ); @@ -122,5 +126,4 @@ public abstract class AbstractEntityService { } } } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index 6433db5ae8..4335fa5f1c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; @@ -43,6 +44,9 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -97,13 +101,29 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService { + private final TenantId tenantId; + private final EntityId entityId; + private final EdgeId edgeId; + private final T entity; + + @Builder.Default + private final long ts = System.currentTimeMillis(); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/eventsourcing/RelationActionEvent.java b/dao/src/main/java/org/thingsboard/server/dao/eventsourcing/RelationActionEvent.java new file mode 100644 index 0000000000..81437cd781 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/eventsourcing/RelationActionEvent.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.eventsourcing; + +import lombok.Data; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.relation.EntityRelation; + +@Data +public class RelationActionEvent { + private final TenantId tenantId; + private final EntityRelation relation; + private final ActionType actionType; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/eventsourcing/SaveEntityEvent.java b/dao/src/main/java/org/thingsboard/server/dao/eventsourcing/SaveEntityEvent.java new file mode 100644 index 0000000000..205f592d43 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/eventsourcing/SaveEntityEvent.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.eventsourcing; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +@Builder +@Data +public class SaveEntityEvent { + private final TenantId tenantId; + private final T entity; + private final EntityId entityId; + private final Boolean added; +} 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 552f83c749..0e8ea2db15 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 @@ -308,13 +308,22 @@ public class ModelConstants { */ public static final String WIDGET_TYPE_TABLE_NAME = "widget_type"; public static final String WIDGET_TYPE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; - public static final String WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY = "bundle_alias"; - public static final String WIDGET_TYPE_ALIAS_PROPERTY = ALIAS_PROPERTY; + + public static final String WIDGET_TYPE_FQN_PROPERTY = "fqn"; public static final String WIDGET_TYPE_NAME_PROPERTY = "name"; public static final String WIDGET_TYPE_IMAGE_PROPERTY = "image"; public static final String WIDGET_TYPE_DESCRIPTION_PROPERTY = "description"; public static final String WIDGET_TYPE_DESCRIPTOR_PROPERTY = "descriptor"; + public static final String WIDGET_TYPE_DEPRECATED_PROPERTY = "deprecated"; + + /** + * Widgets bundle widget constants. + */ + public static final String WIDGETS_BUNDLE_WIDGET_TABLE_NAME = "widgets_bundle_widget"; + + public static final String WIDGET_TYPE_ORDER_PROPERTY = "widget_type_order"; + /** * Dashboard constants. */ 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 127e8509b7..08b2ebed77 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 @@ -36,15 +36,15 @@ public abstract class AbstractWidgetTypeEntity extends @Column(name = ModelConstants.WIDGET_TYPE_TENANT_ID_PROPERTY) private UUID tenantId; - @Column(name = ModelConstants.WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY) - private String bundleAlias; - - @Column(name = ModelConstants.WIDGET_TYPE_ALIAS_PROPERTY) - private String alias; + @Column(name = ModelConstants.WIDGET_TYPE_FQN_PROPERTY) + private String fqn; @Column(name = ModelConstants.WIDGET_TYPE_NAME_PROPERTY) private String name; + @Column(name = ModelConstants.WIDGET_TYPE_DEPRECATED_PROPERTY) + private boolean deprecated; + public AbstractWidgetTypeEntity() { super(); } @@ -57,18 +57,18 @@ public abstract class AbstractWidgetTypeEntity extends if (widgetType.getTenantId() != null) { this.tenantId = widgetType.getTenantId().getId(); } - this.bundleAlias = widgetType.getBundleAlias(); - this.alias = widgetType.getAlias(); + this.fqn = widgetType.getFqn(); this.name = widgetType.getName(); + this.deprecated = widgetType.isDeprecated(); } public AbstractWidgetTypeEntity(AbstractWidgetTypeEntity widgetTypeEntity) { this.setId(widgetTypeEntity.getId()); this.setCreatedTime(widgetTypeEntity.getCreatedTime()); this.tenantId = widgetTypeEntity.getTenantId(); - this.bundleAlias = widgetTypeEntity.getBundleAlias(); - this.alias = widgetTypeEntity.getAlias(); + this.fqn = widgetTypeEntity.getFqn(); this.name = widgetTypeEntity.getName(); + this.deprecated = widgetTypeEntity.isDeprecated(); } protected BaseWidgetType toBaseWidgetType() { @@ -77,9 +77,9 @@ public abstract class AbstractWidgetTypeEntity extends if (tenantId != null) { widgetType.setTenantId(TenantId.fromUUID(tenantId)); } - widgetType.setBundleAlias(bundleAlias); - widgetType.setAlias(alias); + widgetType.setFqn(fqn); widgetType.setName(name); + widgetType.setDeprecated(deprecated); return widgetType; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeDetailsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeDetailsEntity.java index c936952b40..be934e9c8f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeDetailsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeDetailsEntity.java @@ -20,6 +20,8 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.BaseWidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.dao.model.ModelConstants; @@ -28,6 +30,7 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; +import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) @@ -46,6 +49,9 @@ public class WidgetTypeDetailsEntity extends AbstractWidgetTypeEntity { + + @Id + @Column(name = "widgets_bundle_id", columnDefinition = "uuid") + private UUID widgetsBundleId; + + @Id + @Column(name = "widget_type_id", columnDefinition = "uuid") + private UUID widgetTypeId; + + @Column(name = WIDGET_TYPE_ORDER_PROPERTY) + private int widgetTypeOrder; + + public WidgetsBundleWidgetEntity() { + super(); + } + + public WidgetsBundleWidgetEntity(WidgetsBundleWidget widgetsBundleWidget) { + widgetsBundleId = widgetsBundleWidget.getWidgetsBundleId().getId(); + widgetTypeId = widgetsBundleWidget.getWidgetTypeId().getId(); + widgetTypeOrder = widgetsBundleWidget.getWidgetTypeOrder(); + } + + public WidgetsBundleWidgetEntity(UUID widgetsBundleId, UUID widgetTypeId, int widgetTypeOrder) { + this.widgetsBundleId = widgetsBundleId; + this.widgetTypeId = widgetTypeId; + this.widgetTypeOrder = widgetTypeOrder; + } + + @Override + public WidgetsBundleWidget toData() { + WidgetsBundleWidget result = new WidgetsBundleWidget(); + result.setWidgetsBundleId(new WidgetsBundleId(widgetsBundleId)); + result.setWidgetTypeId(new WidgetTypeId(widgetTypeId)); + result.setWidgetTypeOrder(widgetTypeOrder); + return result; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java index 425724bf2c..92289b7178 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java @@ -19,13 +19,13 @@ import com.google.common.base.Function; import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.springframework.beans.factory.annotation.Value; +import org.thingsboard.common.util.ThingsBoardExecutors; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; /** * Created by ashvayka on 21.02.17. @@ -34,9 +34,12 @@ public abstract class CassandraAbstractAsyncDao extends CassandraAbstractDao { protected ExecutorService readResultsProcessingExecutor; + @Value("${cassandra.query.result_processing_threads:50}") + private int threadPoolSize; + @PostConstruct public void startExecutor() { - readResultsProcessingExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("cassandra-callback")); + readResultsProcessingExecutor = ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "cassandra-callback"); } @PreDestroy diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java index 4262decfed..4faed9cb20 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.notification; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @@ -25,8 +26,11 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings.NotificationPref; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.platform.AffectedTenantAdministratorsFilter; import org.thingsboard.server.common.data.notification.targets.platform.AffectedUserFilter; @@ -38,20 +42,28 @@ import org.thingsboard.server.common.data.notification.targets.platform.TenantAd import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.user.UserSettingsService; import java.util.Collections; +import java.util.EnumMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; @Service @RequiredArgsConstructor +@Slf4j public class DefaultNotificationSettingsService implements NotificationSettingsService { private final AdminSettingsService adminSettingsService; private final NotificationTargetService notificationTargetService; private final NotificationTemplateService notificationTemplateService; private final DefaultNotifications defaultNotifications; + private final UserSettingsService userSettingsService; private static final String SETTINGS_KEY = "notifications"; @@ -81,6 +93,58 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS }); } + @Override + public UserNotificationSettings saveUserNotificationSettings(TenantId tenantId, UserId userId, UserNotificationSettings settings) { + UserSettings userSettings = new UserSettings(); + userSettings.setUserId(userId); + userSettings.setType(UserSettingsType.NOTIFICATIONS); + userSettings.setSettings(JacksonUtil.valueToTree(settings)); + userSettingsService.saveUserSettings(tenantId, userSettings); + return formatUserNotificationSettings(settings); + } + + @Override + public UserNotificationSettings getUserNotificationSettings(TenantId tenantId, UserId userId, boolean format) { + UserSettings userSettings = userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.NOTIFICATIONS); + UserNotificationSettings settings = null; + if (userSettings != null) { + try { + settings = JacksonUtil.treeToValue(userSettings.getSettings(), UserNotificationSettings.class); + } catch (Exception e) { + log.warn("Failed to parse notification settings for user {}", userId, e); + } + } + if (settings == null) { + settings = UserNotificationSettings.DEFAULT; + } + if (format) { + settings = formatUserNotificationSettings(settings); + } + return settings; + } + + private UserNotificationSettings formatUserNotificationSettings(UserNotificationSettings settings) { + Map prefs = new EnumMap<>(NotificationType.class); + if (settings != null) { + prefs.putAll(settings.getPrefs()); + } + NotificationPref defaultPref = NotificationPref.createDefault(); + for (NotificationType notificationType : NotificationType.values()) { + NotificationPref pref = prefs.get(notificationType); + if (pref == null) { + prefs.put(notificationType, defaultPref); + } else { + var enabledDeliveryMethods = new LinkedHashMap<>(pref.getEnabledDeliveryMethods()); + // in case a new delivery method was added to the platform + UserNotificationSettings.deliveryMethods.forEach(deliveryMethod -> { + enabledDeliveryMethods.putIfAbsent(deliveryMethod, true); + }); + pref.setEnabledDeliveryMethods(enabledDeliveryMethods); + } + } + return new UserNotificationSettings(prefs); + } + @Transactional(propagation = Propagation.NOT_SUPPORTED) // so that parent transaction is not aborted on method failure @Override public void createDefaultNotificationConfigs(TenantId tenantId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index a6c558a388..29c7928a24 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -66,6 +66,9 @@ import static org.thingsboard.server.dao.DaoUtil.toUUIDs; @RequiredArgsConstructor public class DefaultNotifications { + private static final String YELLOW_COLOR = "#F9D916"; + private static final String RED_COLOR = "#e91a1a"; + public static final DefaultNotification maintenanceWork = DefaultNotification.builder() .name("Maintenance work notification") .subject("Infrastructure maintenance") @@ -77,7 +80,7 @@ public class DefaultNotifications { .type(NotificationType.ENTITIES_LIMIT) .subject("${entityType}s limit will be reached soon for tenant ${tenantName}") .text("${entityType}s usage: ${currentCount}/${limit} (${percents}%)") - .icon("warning").color("#F9D916") + .icon("warning").color(YELLOW_COLOR) .rule(DefaultRule.builder() .name("Entities count limit (sysadmin)") .triggerConfig(EntitiesLimitNotificationRuleTriggerConfig.builder() @@ -100,7 +103,7 @@ public class DefaultNotifications { .type(NotificationType.API_USAGE_LIMIT) .subject("${feature} feature will be disabled soon for tenant ${tenantName}") .text("Usage: ${currentValue} out of ${limit} ${unitLabel}s") - .icon("warning").color("#F9D916") + .icon("warning").color(YELLOW_COLOR) .rule(DefaultRule.builder() .name("API feature warning (sysadmin)") .triggerConfig(ApiUsageLimitNotificationRuleTriggerConfig.builder() @@ -123,7 +126,7 @@ public class DefaultNotifications { .type(NotificationType.API_USAGE_LIMIT) .subject("${feature} feature was disabled for tenant ${tenantName}") .text("Used ${currentValue} out of ${limit} ${unitLabel}s") - .icon("block").color("#e91a1a") + .icon("block").color(RED_COLOR) .rule(DefaultRule.builder() .name("API feature disabled (sysadmin)") .triggerConfig(ApiUsageLimitNotificationRuleTriggerConfig.builder() @@ -147,7 +150,7 @@ public class DefaultNotifications { .type(NotificationType.RATE_LIMITS) .subject("Rate limits exceeded") .text("Rate limits for ${api} exceeded") - .icon("block").color("#e91a1a") + .icon("block").color(RED_COLOR) .rule(DefaultRule.builder() .name("Per-tenant rate limits exceeded") .triggerConfig(RateLimitsNotificationRuleTriggerConfig.builder() @@ -164,7 +167,7 @@ public class DefaultNotifications { .type(NotificationType.RATE_LIMITS) .subject("Rate limits exceeded") .text("Rate limits for ${api} exceeded for '${limitLevelEntityName}'") - .icon("block").color("#e91a1a") + .icon("block").color(RED_COLOR) .rule(DefaultRule.builder() .name("Per-entity rate limits exceeded") .triggerConfig(RateLimitsNotificationRuleTriggerConfig.builder() @@ -323,6 +326,15 @@ public class DefaultNotifications { .build()) .build(); + public static final DefaultNotification jwtSigningKeyIssue = DefaultNotification.builder() + .name("JWT Signing Key issue notification") + .type(NotificationType.GENERAL) + .subject("WARNING: security issue") + .text("The platform is configured to use default JWT Signing Key. Please change it on the security settings page") + .icon("warning").color(YELLOW_COLOR) + .button("Go to settings").link("/security-settings/general") + .build(); + private final NotificationTemplateService templateService; private final NotificationRuleService ruleService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 2a784d7985..13f84161c8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -38,6 +38,8 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -77,10 +79,12 @@ public class BaseOtaPackageService extends AbstractCachedEntityService handleEvictEvent(EntityRelationEvent.from(relation)), MoreExecutors.directExecutor()); + future.addListener(() -> { + handleEvictEvent(EntityRelationEvent.from(relation)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_ADD_OR_UPDATE)); + }, MoreExecutors.directExecutor()); return future; } @@ -188,6 +195,7 @@ public class BaseRelationService implements RelationService { var result = relationDao.deleteRelation(tenantId, relation); //TODO: evict cache only if the relation was deleted. Note: relationDao.deleteRelation requires improvement. publishEvictEvent(EntityRelationEvent.from(relation)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); return result; } @@ -196,7 +204,10 @@ public class BaseRelationService implements RelationService { log.trace("Executing deleteRelationAsync [{}]", relation); validate(relation); var future = relationDao.deleteRelationAsync(tenantId, relation); - future.addListener(() -> handleEvictEvent(EntityRelationEvent.from(relation)), MoreExecutors.directExecutor()); + future.addListener(() -> { + handleEvictEvent(EntityRelationEvent.from(relation)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); + }, MoreExecutors.directExecutor()); return future; } @@ -206,7 +217,9 @@ public class BaseRelationService implements RelationService { validate(from, to, relationType, typeGroup); var result = relationDao.deleteRelation(tenantId, from, to, relationType, typeGroup); //TODO: evict cache only if the relation was deleted. Note: relationDao.deleteRelation requires improvement. - publishEvictEvent(new EntityRelationEvent(from, to, relationType, typeGroup)); + EntityRelation entityRelation = new EntityRelation(from, to, relationType, typeGroup); + publishEvictEvent(EntityRelationEvent.from(entityRelation)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, entityRelation, ActionType.RELATION_DELETED)); return result; } @@ -220,17 +233,36 @@ public class BaseRelationService implements RelationService { return future; } + @Transactional + @Override + public void deleteEntityCommonRelations(TenantId tenantId, EntityId entityId) { + deleteEntityRelations(tenantId, entityId, RelationTypeGroup.COMMON); + } + @Transactional @Override public void deleteEntityRelations(TenantId tenantId, EntityId entityId) { + deleteEntityRelations(tenantId, entityId, null); + } + + @Transactional + public void deleteEntityRelations(TenantId tenantId, EntityId entityId, RelationTypeGroup relationTypeGroup) { log.trace("Executing deleteEntityRelations [{}]", entityId); validate(entityId); - List inboundRelations = new ArrayList<>(relationDao.findAllByTo(tenantId, entityId)); - List outboundRelations = new ArrayList<>(relationDao.findAllByFrom(tenantId, entityId)); + List inboundRelations = relationTypeGroup == null + ? relationDao.findAllByTo(tenantId, entityId) + : relationDao.findAllByTo(tenantId, entityId, relationTypeGroup); + List outboundRelations = relationTypeGroup == null + ? relationDao.findAllByFrom(tenantId, entityId) + : relationDao.findAllByFrom(tenantId, entityId, relationTypeGroup); if (!inboundRelations.isEmpty()) { try { - relationDao.deleteInboundRelations(tenantId, entityId); + if (relationTypeGroup == null) { + relationDao.deleteInboundRelations(tenantId, entityId); + } else { + relationDao.deleteInboundRelations(tenantId, entityId, relationTypeGroup); + } } catch (ConcurrencyFailureException e) { log.debug("Concurrency exception while deleting relations [{}]", inboundRelations, e); } @@ -241,7 +273,11 @@ public class BaseRelationService implements RelationService { } if (!outboundRelations.isEmpty()) { - relationDao.deleteOutboundRelations(tenantId, entityId); + if (relationTypeGroup == null) { + relationDao.deleteOutboundRelations(tenantId, entityId); + } else { + relationDao.deleteOutboundRelations(tenantId, entityId, relationTypeGroup); + } for (EntityRelation relation : outboundRelations) { eventPublisher.publishEvent(EntityRelationEvent.from(relation)); @@ -634,5 +670,4 @@ public class BaseRelationService implements RelationService { handleEvictEvent(event); } } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index 7fee4a31ff..250a0c6105 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -64,8 +64,12 @@ public interface RelationDao { void deleteOutboundRelations(TenantId tenantId, EntityId entity); + void deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup); + void deleteInboundRelations(TenantId tenantId, EntityId entity); + void deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup); + ListenableFuture deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity); List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 56e6969c9a..5e6b1e8009 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -27,10 +27,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbVersionedNode; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; @@ -54,16 +53,17 @@ import org.thingsboard.server.common.data.rule.RuleChainUpdateResult; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNodeUpdateResult; import org.thingsboard.server.common.data.util.ReflectionUtils; -import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityCountService; +import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.RuleChainDataValidator; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -114,6 +114,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (ruleChain.getId() == null) { entityCountService.publishCountEntityEvictEvent(ruleChain.getTenantId(), EntityType.RULE_CHAIN); } + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedRuleChain.getTenantId()) + .entity(savedRuleChain).entityId(savedRuleChain.getId()).added(ruleChain.getId() == null).build()); return savedRuleChain; } catch (Exception e) { checkConstraintViolation(e, "rule_chain_external_id_unq_key", "Rule Chain with such external id already exists!"); @@ -259,6 +261,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (!relations.isEmpty()) { relationService.saveRelations(tenantId, relations); } + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChain).entityId(ruleChain.getId()).build()); return RuleChainUpdateResult.successful(updatedRuleNodes); } @@ -594,6 +597,10 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC log.warn("[{}] Failed to create ruleChain relation. Edge Id: [{}]", ruleChainId, edgeId); throw new RuntimeException(e); } + if (!ruleChainId.equals(edge.getRootRuleChainId())) { + eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).edgeId(edgeId).entityId(ruleChainId) + .actionType(ActionType.ASSIGNED_TO_EDGE).build()); + } return ruleChain; } @@ -613,6 +620,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC log.warn("[{}] Failed to delete rule chain relation. Edge Id: [{}]", ruleChainId, edgeId); throw new RuntimeException(e); } + eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).edgeId(edgeId).entityId(ruleChainId) + .actionType(ActionType.UNASSIGNED_FROM_EDGE).build()); return ruleChain; } @@ -726,6 +735,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC try { entityCountService.publishCountEntityEvictEvent(tenantId, EntityType.RULE_CHAIN); ruleChainDao.removeById(tenantId, ruleChainId.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(ruleChainId).build()); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_rule_chain_device_profile")) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index e4bc622354..d81c05044f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -86,6 +86,15 @@ public abstract class DataValidator> { public void validateDelete(TenantId tenantId, EntityId entityId) { } + public void validateString(String exceptionPrefix, String name) { + if (StringUtils.isEmpty(name) || name.trim().length() == 0) { + throw new DataValidationException(exceptionPrefix + " should be specified!"); + } + if (StringUtils.contains0x00(name)) { + throw new DataValidationException(exceptionPrefix + " should not contain 0x00 symbol!"); + } + } + protected boolean isSameData(D existentData, D actualData) { return actualData.getId() != null && existentData.getId().equals(actualData.getId()); } @@ -151,8 +160,8 @@ public abstract class DataValidator> { validateQueueNameOrTopic(topic, TOPIC); } - private static void validateQueueNameOrTopic(String value, String fieldName) { - if (StringUtils.isEmpty(value)) { + static void validateQueueNameOrTopic(String value, String fieldName) { + if (StringUtils.isEmpty(value) || value.trim().length() == 0 ) { throw new DataValidationException(String.format("Queue %s should be specified!", fieldName)); } if (!QUEUE_PATTERN.matcher(value).matches()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java index 692690f328..9d0d53732c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.service.validator; import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -49,12 +48,9 @@ public class AdminSettingsDataValidator extends DataValidator { return existentAdminSettings; } - @Override protected void validateDataImpl(TenantId tenantId, AdminSettings adminSettings) { - if (StringUtils.isEmpty(adminSettings.getKey())) { - throw new DataValidationException("Key should be specified!"); - } + validateString("Key", adminSettings.getKey()); if (adminSettings.getJsonValue() == null) { throw new DataValidationException("Json value should be specified!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmDataValidator.java index 2c76fd2bff..6ef2eb5052 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmDataValidator.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.service.validator; import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.DataValidationException; @@ -32,9 +31,7 @@ public class AlarmDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, Alarm alarm) { - if (StringUtils.isEmpty(alarm.getType())) { - throw new DataValidationException("Alarm type should be specified!"); - } + validateString("Alarm type", alarm.getType()); if (alarm.getOriginator() == null) { throw new DataValidationException("Alarm originator should be specified!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java index 57ae7d3c5e..6c0d862c0d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java @@ -20,7 +20,6 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -64,9 +63,7 @@ public class AssetDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, Asset asset) { - if (StringUtils.isEmpty(asset.getName())) { - throw new DataValidationException("Asset name should be specified!"); - } + validateString("Asset name", asset.getName()); if (asset.getTenantId() == null) { throw new DataValidationException("Asset should be assigned to tenant!"); } else { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java index d206dfefba..f0816621e0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java @@ -53,9 +53,7 @@ public class AssetProfileDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, AssetProfile assetProfile) { - if (StringUtils.isEmpty(assetProfile.getName())) { - throw new DataValidationException("Asset profile name should be specified!"); - } + validateString("Asset profile name", assetProfile.getName()); if (assetProfile.getTenantId() == null) { throw new DataValidationException("Asset profile should be assigned to tenant!"); } else { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidator.java index df37676688..5befacf8ea 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidator.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.service.validator; +import lombok.Getter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.thingsboard.server.common.data.BaseData; @@ -31,23 +32,27 @@ import java.util.Objects; public abstract class BaseOtaPackageDataValidator> extends DataValidator { @Autowired + @Getter @Lazy private TenantService tenantService; @Autowired + @Getter private DeviceProfileDao deviceProfileDao; protected void validateImpl(OtaPackageInfo otaPackageInfo) { + validateString("OtaPackage title", otaPackageInfo.getTitle()); + if (otaPackageInfo.getTenantId() == null) { throw new DataValidationException("OtaPackage should be assigned to tenant!"); } else { - if (!tenantService.tenantExists(otaPackageInfo.getTenantId())) { + if (!getTenantService().tenantExists(otaPackageInfo.getTenantId())) { throw new DataValidationException("OtaPackage is referencing to non-existent tenant!"); } } if (otaPackageInfo.getDeviceProfileId() != null) { - DeviceProfile deviceProfile = deviceProfileDao.findById(otaPackageInfo.getTenantId(), otaPackageInfo.getDeviceProfileId().getId()); + DeviceProfile deviceProfile = getDeviceProfileDao().findById(otaPackageInfo.getTenantId(), otaPackageInfo.getDeviceProfileId().getId()); if (deviceProfile == null) { throw new DataValidationException("OtaPackage is referencing to non-existent device profile!"); } @@ -57,10 +62,6 @@ public abstract class BaseOtaPackageDataValidator> extends throw new DataValidationException("Type should be specified!"); } - if (StringUtils.isEmpty(otaPackageInfo.getTitle())) { - throw new DataValidationException("OtaPackage title should be specified!"); - } - if (StringUtils.isEmpty(otaPackageInfo.getVersion())) { throw new DataValidationException("OtaPackage version should be specified!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidator.java index 831b81a6fc..446091a698 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidator.java @@ -27,15 +27,13 @@ public class ComponentDescriptorDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, Customer customer) { - if (StringUtils.isEmpty(customer.getTitle())) { - throw new DataValidationException("Customer title should be specified!"); - } + validateString("Customer title", customer.getTitle()); if (customer.getTitle().equals(CustomerServiceImpl.PUBLIC_CUSTOMER_TITLE)) { throw new DataValidationException("'Public' title for customer is system reserved!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java index 246944199a..60e1045f07 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java @@ -19,9 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.dashboard.DashboardDao; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; @@ -29,9 +27,6 @@ import org.thingsboard.server.dao.tenant.TenantService; @Component public class DashboardDataValidator extends DataValidator { - @Autowired - private DashboardDao dashboardDao; - @Autowired private TenantService tenantService; @@ -42,9 +37,7 @@ public class DashboardDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, Dashboard dashboard) { - if (StringUtils.isEmpty(dashboard.getTitle())) { - throw new DataValidationException("Dashboard title should be specified!"); - } + validateString("Dashboard title", dashboard.getTitle()); if (dashboard.getTenantId() == null) { throw new DataValidationException("Dashboard should be assigned to tenant!"); } else { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java index 51517a4dc2..41cdcb0b2e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java @@ -20,7 +20,6 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.data.DeviceTransportConfiguration; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -61,9 +60,7 @@ public class DeviceDataValidator extends AbstractHasOtaPackageValidator @Override protected void validateDataImpl(TenantId tenantId, Device device) { - if (StringUtils.isEmpty(device.getName()) || device.getName().trim().length() == 0) { - throw new DataValidationException("Device name should be specified!"); - } + validateString("Device name", device.getName()); if (device.getTenantId() == null) { throw new DataValidationException("Device should be assigned to tenant!"); } else { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java index bd4bf04c45..89510c6076 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java @@ -103,9 +103,7 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator { @Override protected void validateDataImpl(TenantId tenantId, Edge edge) { - if (StringUtils.isEmpty(edge.getType())) { - throw new DataValidationException("Edge type should be specified!"); - } - if (StringUtils.isEmpty(edge.getName())) { - throw new DataValidationException("Edge name should be specified!"); - } + validateString("Edge name", edge.getName()); + validateString("Edge type", edge.getType()); if (StringUtils.isEmpty(edge.getSecret())) { throw new DataValidationException("Edge secret should be specified!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java index dddae3195d..7cc72a67cf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java @@ -19,7 +19,6 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.customer.CustomerDao; @@ -59,12 +58,8 @@ public class EntityViewDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, EntityView entityView) { - if (StringUtils.isEmpty(entityView.getType())) { - throw new DataValidationException("Entity View type should be specified!"); - } - if (StringUtils.isEmpty(entityView.getName())) { - throw new DataValidationException("Entity view name should be specified!"); - } + validateString("Entity view name", entityView.getName()); + validateString("Entity view type", entityView.getType()); if (entityView.getTenantId() == null) { throw new DataValidationException("Entity view should be assigned to tenant!"); } else { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java index 9939d887fa..d620950628 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java @@ -66,9 +66,7 @@ public class ResourceDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, TbResource resource) { - if (StringUtils.isEmpty(resource.getTitle())) { - throw new DataValidationException("Resource title should be specified!"); - } + validateString("Resource title", resource.getTitle()); if (resource.getResourceType() == null) { throw new DataValidationException("Resource type should be specified!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java index 5450e29706..08e047904f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java @@ -23,7 +23,6 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; @@ -65,9 +64,7 @@ public class RuleChainDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, RuleChain ruleChain) { - if (StringUtils.isEmpty(ruleChain.getName())) { - throw new DataValidationException("Rule chain name should be specified!"); - } + validateString("Rule chain name", ruleChain.getName()); if (ruleChain.getType() == null) { ruleChain.setType(RuleChainType.CORE); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantDataValidator.java index c6e8e055a5..64dff6495a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantDataValidator.java @@ -32,9 +32,7 @@ public class TenantDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, Tenant tenant) { - if (StringUtils.isEmpty(tenant.getTitle())) { - throw new DataValidationException("Tenant title should be specified!"); - } + validateString("Tenant title", tenant.getTitle()); if (!StringUtils.isEmpty(tenant.getEmail())) { validateEmail(tenant.getEmail()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java index 0477ccf32f..a46371de39 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java @@ -19,7 +19,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.ProcessingStrategy; @@ -48,9 +47,7 @@ public class TenantProfileDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, TenantProfile tenantProfile) { - if (StringUtils.isEmpty(tenantProfile.getName())) { - throw new DataValidationException("Tenant profile name should be specified!"); - } + validateString("Tenant profile name", tenantProfile.getName()); if (tenantProfile.getProfileData() == null) { throw new DataValidationException("Tenant profile data should be specified!"); } @@ -99,8 +96,6 @@ public class TenantProfileDataValidator extends DataValidator { TenantProfile old = tenantProfileDao.findById(TenantId.SYS_TENANT_ID, tenantProfile.getId().getId()); if (old == null) { throw new DataValidationException("Can't update non existing tenant profile!"); - } else if (old.isIsolatedTbRuleEngine() != tenantProfile.isIsolatedTbRuleEngine()) { - throw new DataValidationException("Can't update isolatedTbRuleEngine property!"); } return old; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java index aa44ff6934..71bc17a8fa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java @@ -17,11 +17,9 @@ package org.thingsboard.server.dao.service.validator; import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; 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.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DataValidator; @@ -39,12 +37,7 @@ public class WidgetTypeDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, WidgetTypeDetails widgetTypeDetails) { - if (StringUtils.isEmpty(widgetTypeDetails.getName())) { - throw new DataValidationException("Widgets type name should be specified!"); - } - if (StringUtils.isEmpty(widgetTypeDetails.getBundleAlias())) { - throw new DataValidationException("Widgets type bundle alias should be specified!"); - } + validateString("Widgets type name", widgetTypeDetails.getName()); if (widgetTypeDetails.getDescriptor() == null || widgetTypeDetails.getDescriptor().size() == 0) { throw new DataValidationException("Widgets type descriptor can't be empty!"); } @@ -60,24 +53,20 @@ public class WidgetTypeDataValidator extends DataValidator { @Override protected void validateCreate(TenantId tenantId, WidgetTypeDetails widgetTypeDetails) { - WidgetsBundle widgetsBundle = widgetsBundleDao.findWidgetsBundleByTenantIdAndAlias(widgetTypeDetails.getTenantId().getId(), widgetTypeDetails.getBundleAlias()); - if (widgetsBundle == null) { - throw new DataValidationException("Widget type is referencing to non-existent widgets bundle!"); + String fqn = widgetTypeDetails.getFqn(); + if (fqn == null || fqn.trim().isEmpty()) { + fqn = widgetTypeDetails.getName().toLowerCase().replaceAll("\\W+", "_"); } - String alias = widgetTypeDetails.getAlias(); - if (alias == null || alias.trim().isEmpty()) { - alias = widgetTypeDetails.getName().toLowerCase().replaceAll("\\W+", "_"); - } - String originalAlias = alias; + String originalFqn = fqn; int c = 1; - WidgetType withSameAlias; + WidgetType withSameFqn; do { - withSameAlias = widgetTypeDao.findByTenantIdBundleAliasAndAlias(widgetTypeDetails.getTenantId().getId(), widgetTypeDetails.getBundleAlias(), alias); - if (withSameAlias != null) { - alias = originalAlias + (++c); + withSameFqn = widgetTypeDao.findByTenantIdAndFqn(widgetTypeDetails.getTenantId().getId(), fqn); + if (withSameFqn != null) { + fqn = originalFqn + (++c); } - } while (withSameAlias != null); - widgetTypeDetails.setAlias(alias); + } while (withSameFqn != null); + widgetTypeDetails.setFqn(fqn); } @Override @@ -86,11 +75,8 @@ public class WidgetTypeDataValidator extends DataValidator { if (!storedWidgetType.getTenantId().getId().equals(widgetTypeDetails.getTenantId().getId())) { throw new DataValidationException("Can't move existing widget type to different tenant!"); } - if (!storedWidgetType.getBundleAlias().equals(widgetTypeDetails.getBundleAlias())) { - throw new DataValidationException("Update of widget type bundle alias is prohibited!"); - } - if (!storedWidgetType.getAlias().equals(widgetTypeDetails.getAlias())) { - throw new DataValidationException("Update of widget type alias is prohibited!"); + if (!storedWidgetType.getFqn().equals(widgetTypeDetails.getFqn())) { + throw new DataValidationException("Update of widget type fqn is prohibited!"); } return new WidgetTypeDetails(storedWidgetType); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java index 8c730398ed..42b60588cb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.service.validator; import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.exception.DataValidationException; @@ -35,9 +34,7 @@ public class WidgetsBundleDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, WidgetsBundle widgetsBundle) { - if (StringUtils.isEmpty(widgetsBundle.getTitle())) { - throw new DataValidationException("Widgets bundle title should be specified!"); - } + validateString("Widgets bundle title", widgetsBundle.getTitle()); if (widgetsBundle.getTenantId() == null) { widgetsBundle.setTenantId(TenantId.fromUUID(ModelConstants.NULL_UUID)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index 92537dc850..30b6c5191d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -18,9 +18,12 @@ package org.thingsboard.server.dao.sql.alarm; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.model.sql.AlarmInfoEntity; @@ -315,6 +318,9 @@ public interface AlarmRepository extends JpaRepository { @Query(value = "SELECT a FROM AlarmInfoEntity a WHERE a.tenantId = :tenantId AND a.id = :alarmId") AlarmInfoEntity findAlarmInfoById(@Param("tenantId") UUID tenantId, @Param("alarmId") UUID alarmId); + @Query("SELECT a.id FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.assigneeId = :assigneeId") + Page findAlarmIdsByAssigneeId(@Param("tenantId") UUID tenantId, @Param("assigneeId") UUID assigneeId, Pageable pageable); + @Query(value = "SELECT create_or_update_active_alarm(:t_id, :c_id, :a_id, :a_created_ts, :a_o_id, :a_o_type, :a_type, :a_severity, " + ":a_start_ts, :a_end_ts, :a_details, :a_propagate, :a_propagate_to_owner, " + ":a_propagate_to_tenant, :a_propagation_types, :a_creation_enabled)", nativeQuery = true) @@ -345,4 +351,13 @@ public interface AlarmRepository extends JpaRepository { @Query(value = "SELECT unassign_alarm(:t_id, :a_id, :a_ts)", nativeQuery = true) String unassignAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long unassignTime); + + @Query(value = "SELECT at.type FROM alarm_types AS at WHERE at.tenant_id = :tenantId AND at.type ILIKE CONCAT('%', :searchText, '%')", nativeQuery = true) + Page findTenantAlarmTypes(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); + + @Transactional + @Modifying + @Query(value = "DELETE FROM alarm_types AS at WHERE NOT EXISTS (SELECT 1 FROM alarm AS a WHERE a.tenant_id = at.tenant_id AND a.type = at.type) AND at.tenant_id = :tenantId AND at.type IN (:types)", nativeQuery = true) + int deleteTypeIfNoAlarmsExist(@Param("tenantId") UUID tenantId, @Param("types") Set types); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index 8dfdaf8a3e..358c4aa019 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -19,11 +19,13 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; @@ -67,6 +69,9 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; +import static org.thingsboard.server.dao.DaoUtil.toPageable; + /** * Created by Valerii Sosliuk on 5/19/2017. */ @@ -285,6 +290,12 @@ public class JpaAlarmDao extends JpaAbstractDao implements A .mapData(AlarmId::new); } + @Override + public PageData findAlarmIdsByAssigneeId(TenantId tenantId, UUID userId, PageLink pageLink) { + return DaoUtil.pageToPageData(alarmRepository.findAlarmIdsByAssigneeId(tenantId.getId(), userId, DaoUtil.toPageable(pageLink))) + .mapData(AlarmId::new); + } + @Override public void createEntityAlarmRecord(EntityAlarm entityAlarm) { log.debug("Saving entity {}", entityAlarm); @@ -366,6 +377,22 @@ public class JpaAlarmDao extends JpaAbstractDao implements A return alarmQueryRepository.countAlarmsByQuery(tenantId, customerId, query); } + @Override + public PageData findTenantAlarmTypes(UUID tenantId, PageLink pageLink) { + Page page = alarmRepository.findTenantAlarmTypes(tenantId, Objects.toString(pageLink.getTextSearch(), ""), toPageable(pageLink)); + if (page.isEmpty()) { + return PageData.emptyPageData(); + } + + List data = convertTenantEntityTypesToDto(tenantId, EntityType.ALARM, page.getContent()); + return new PageData<>(data, page.getTotalPages(), page.getTotalElements(), page.hasNext()); + } + + @Override + public boolean removeAlarmTypesIfNoAlarmsPresent(UUID tenantId, Set types) { + return alarmRepository.deleteTypeIfNoAlarmsExist(tenantId, types) > 0; + } + private static String getPropagationTypes(AlarmPropagationInfo ap) { String propagateRelationTypes; if (!CollectionUtils.isEmpty(ap.getPropagateRelationTypes())) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index ec5a8318f8..4c83815d19 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -37,14 +37,13 @@ import org.thingsboard.server.dao.model.sql.AssetInfoEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; /** @@ -194,7 +193,7 @@ public class JpaAssetDao extends JpaAbstractDao implements A @Override public ListenableFuture> findTenantAssetTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantAssetTypesToDto(tenantId, assetRepository.findTenantAssetTypes(tenantId))); + return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.ASSET, assetRepository.findTenantAssetTypes(tenantId))); } @Override @@ -212,17 +211,6 @@ public class JpaAssetDao extends JpaAbstractDao implements A DaoUtil.toPageable(pageLink))); } - private List convertTenantAssetTypesToDto(UUID tenantId, List types) { - List list = Collections.emptyList(); - if (types != null && !types.isEmpty()) { - list = new ArrayList<>(); - for (String type : types) { - list.add(new EntitySubtype(TenantId.fromUUID(tenantId), EntityType.ASSET, type)); - } - } - return list; - } - @Override public PageData findAssetsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { log.debug("Try to find assets by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index d1ea342bd2..9b63e945ea 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -43,13 +43,13 @@ import org.thingsboard.server.dao.model.sql.DeviceEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; + /** * Created by Valerii Sosliuk on 5/6/2017. */ @@ -217,7 +217,7 @@ public class JpaDeviceDao extends JpaAbstractDao implement @Override public ListenableFuture> findTenantDeviceTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(tenantId))); + return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.DEVICE, deviceRepository.findTenantDeviceTypes(tenantId))); } @Override @@ -240,17 +240,6 @@ public class JpaDeviceDao extends JpaAbstractDao implement return deviceRepository.countByTenantId(tenantId.getId()); } - private List convertTenantDeviceTypesToDto(UUID tenantId, List types) { - List list = Collections.emptyList(); - if (types != null && !types.isEmpty()) { - list = new ArrayList<>(); - for (String type : types) { - list.add(new EntitySubtype(TenantId.fromUUID(tenantId), EntityType.DEVICE, type)); - } - } - return list; - } - @Override public PageData findDevicesByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { log.debug("Try to find devices by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java index 83816fec3a..533421015b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java @@ -120,6 +120,10 @@ public interface EdgeRepository extends JpaRepository { @Param("searchText") String searchText, Pageable pageable); + @Query("SELECT ee FROM EdgeEntity ee, TenantEntity te WHERE ee.tenantId = te.id AND te.tenantProfileId = :tenantProfileId ") + Page findByTenantProfileId(@Param("tenantProfileId") UUID tenantProfileId, + Pageable pageable); + @Query("SELECT DISTINCT d.type FROM EdgeEntity d WHERE d.tenantId = :tenantId") List findTenantEdgeTypes(@Param("tenantId") UUID tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java index c40ca2ca6d..c61040c000 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java @@ -34,13 +34,13 @@ import org.thingsboard.server.dao.model.sql.EdgeInfoEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; + @Component @Slf4j @SqlDao @@ -144,7 +144,7 @@ public class JpaEdgeDao extends JpaAbstractDao implements Edge @Override public ListenableFuture> findTenantEdgeTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantEdgeTypesToDto(tenantId, edgeRepository.findTenantEdgeTypes(tenantId))); + return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.EDGE, edgeRepository.findTenantEdgeTypes(tenantId))); } @Override @@ -184,15 +184,13 @@ public class JpaEdgeDao extends JpaAbstractDao implements Edge DaoUtil.toPageable(pageLink))); } - private List convertTenantEdgeTypesToDto(UUID tenantId, List types) { - List list = Collections.emptyList(); - if (types != null && !types.isEmpty()) { - list = new ArrayList<>(); - for (String type : types) { - list.add(new EntitySubtype(TenantId.fromUUID(tenantId), EntityType.EDGE, type)); - } - } - return list; + @Override + public PageData findEdgesByTenantProfileId(UUID tenantProfileId, PageLink pageLink) { + log.debug("Try to find edges by tenantProfileId [{}], pageLink [{}]", tenantProfileId, pageLink); + return DaoUtil.toPageData( + edgeRepository.findByTenantProfileId( + tenantProfileId, + DaoUtil.toPageable(pageLink))); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index f3f82d34da..e3e9edf80d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -35,13 +35,13 @@ import org.thingsboard.server.dao.model.sql.EntityViewInfoEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; + /** * Created by Victor Basanets on 8/31/2017. */ @@ -167,18 +167,7 @@ public class JpaEntityViewDao extends JpaAbstractDao> findTenantEntityViewTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantEntityViewTypesToDto(tenantId, entityViewRepository.findTenantEntityViewTypes(tenantId))); - } - - private List convertTenantEntityViewTypesToDto(UUID tenantId, List types) { - List list = Collections.emptyList(); - if (types != null && !types.isEmpty()) { - list = new ArrayList<>(); - for (String type : types) { - list.add(new EntitySubtype(TenantId.fromUUID(tenantId), EntityType.ENTITY_VIEW, type)); - } - } - return list; + return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.ENTITY_VIEW, entityViewRepository.findTenantEntityViewTypes(tenantId))); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index f35e7e608f..cf2ff1e7a0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.event.RuleChainDebugEventFilter; import org.thingsboard.server.common.data.event.RuleNodeDebugEventFilter; import org.thingsboard.server.common.data.event.StatisticsEventFilter; import org.thingsboard.server.common.data.id.EventId; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.stats.StatsFactory; @@ -280,7 +281,7 @@ public class JpaBaseEventDao implements EventDao { private PageData findEventByFilter(UUID tenantId, UUID entityId, LifeCycleEventFilter eventFilter, TimePageLink pageLink) { boolean statusFilterEnabled = !StringUtils.isEmpty(eventFilter.getStatus()); - boolean statusFilter = statusFilterEnabled && eventFilter.getStatus().equalsIgnoreCase("Success"); + boolean statusFilter = statusFilterEnabled && eventFilter.getStatus().equalsIgnoreCase(TbNodeConnectionType.SUCCESS); return DaoUtil.toPageData( lcEventRepository.findEvents( tenantId, @@ -359,7 +360,7 @@ public class JpaBaseEventDao implements EventDao { private void removeEventsByFilter(UUID tenantId, UUID entityId, LifeCycleEventFilter eventFilter, Long startTime, Long endTime) { boolean statusFilterEnabled = !StringUtils.isEmpty(eventFilter.getStatus()); - boolean statusFilter = statusFilterEnabled && eventFilter.getStatus().equalsIgnoreCase("Success"); + boolean statusFilter = statusFilterEnabled && eventFilter.getStatus().equalsIgnoreCase(TbNodeConnectionType.SUCCESS); lcEventRepository.removeEvents( tenantId, entityId, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java index 5fa156725d..7eeab8c592 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java @@ -81,6 +81,9 @@ public class JpaNotificationDao extends JpaAbstractDao deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity) { return service.submit( diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java index a3d6d8570d..10c8c826eb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java @@ -82,4 +82,9 @@ public interface RelationRepository @Query("DELETE FROM RelationEntity r where r.toId = :toId and r.toType = :toType and r.relationTypeGroup in :relationTypeGroups") void deleteByToIdAndToTypeAndRelationTypeGroupIn(@Param("toId") UUID toId, @Param("toType") String toType, @Param("relationTypeGroups") List relationTypeGroups); + @Transactional + @Modifying + @Query("DELETE FROM RelationEntity r where r.fromId = :fromId and r.fromType = :fromType and r.relationTypeGroup in :relationTypeGroups") + void deleteByFromIdAndFromTypeAndRelationTypeGroupIn(@Param("fromId") UUID fromId, @Param("fromType") String fromType, @Param("relationTypeGroups") List relationTypeGroups); + } 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 689bcb0b03..0548ad814d 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 @@ -20,17 +20,29 @@ 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.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; +import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.WidgetTypeDetailsEntity; +import org.thingsboard.server.dao.model.sql.WidgetsBundleWidgetCompositeKey; +import org.thingsboard.server.dao.model.sql.WidgetsBundleWidgetEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.widget.WidgetTypeDao; +import java.util.Comparator; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; + +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; /** * Created by Valerii Sosliuk on 4/29/2017. @@ -42,6 +54,9 @@ public class JpaWidgetTypeDao extends JpaAbstractDao getEntityClass() { return WidgetTypeDetailsEntity.class; @@ -58,23 +73,67 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findWidgetTypesByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias) { - return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesByTenantIdAndBundleAlias(tenantId, bundleAlias)); + public boolean existsByTenantIdAndId(TenantId tenantId, UUID widgetTypeId) { + return widgetTypeRepository.existsByTenantIdAndId(tenantId.getId(), widgetTypeId); + } + + @Override + public PageData findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, PageLink pageLink) { + return DaoUtil.toPageData( + widgetTypeRepository + .findSystemWidgetTypes( + NULL_UUID, + Objects.toString(pageLink.getTextSearch(), ""), + fullSearch, + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink) { + return DaoUtil.toPageData( + widgetTypeRepository + .findAllTenantWidgetTypesByTenantId( + tenantId, + NULL_UUID, + Objects.toString(pageLink.getTextSearch(), ""), + fullSearch, + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink) { + return DaoUtil.toPageData( + widgetTypeRepository + .findTenantWidgetTypesByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + fullSearch, + DaoUtil.toPageable(pageLink))); + } + + @Override + public List findWidgetTypesByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId) { + return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesByWidgetsBundleId(widgetsBundleId)); } @Override - public List findWidgetTypesDetailsByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias) { - return DaoUtil.convertDataList(widgetTypeRepository.findByTenantIdAndBundleAlias(tenantId, bundleAlias)); + public List findWidgetTypesDetailsByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId) { + return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesDetailsByWidgetsBundleId(widgetsBundleId)); } @Override - public List findWidgetTypesInfosByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias) { - return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId, bundleAlias)); + public List findWidgetTypesInfosByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId) { + return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByWidgetsBundleId(widgetsBundleId)); } @Override - public WidgetType findByTenantIdBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias) { - return DaoUtil.getData(widgetTypeRepository.findWidgetTypeByTenantIdAndBundleAliasAndAlias(tenantId, bundleAlias, alias)); + public List findWidgetFqnsByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId) { + return widgetTypeRepository.findWidgetFqnsByWidgetsBundleId(widgetsBundleId); + } + + @Override + public WidgetType findByTenantIdAndFqn(UUID tenantId, String fqn) { + return DaoUtil.getData(widgetTypeRepository.findWidgetTypeByTenantIdAndFqn(tenantId, fqn)); } @Override @@ -82,9 +141,54 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findWidgetTypeIdsByTenantIdAndFqns(UUID tenantId, List widgetFqns) { + var idFqnPairs = widgetTypeRepository.findWidgetTypeIdsByTenantIdAndFqns(tenantId, widgetFqns); + idFqnPairs.sort(Comparator.comparingInt(o -> widgetFqns.indexOf(o.getFqn()))); + return idFqnPairs.stream() + .map(id -> new WidgetTypeId(id.getId())).collect(Collectors.toList()); + } + + @Override + public WidgetTypeDetails findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { + return DaoUtil.getData(widgetTypeRepository.findByTenantIdAndExternalId(tenantId, externalId)); + } + + @Override + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + widgetTypeRepository + .findTenantWidgetTypeDetailsByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public WidgetTypeId getExternalIdByInternal(WidgetTypeId internalId) { + return Optional.ofNullable(widgetTypeRepository.getExternalIdById(internalId.getId())) + .map(WidgetTypeId::new).orElse(null); + } + + @Override + public List findWidgetsBundleWidgetsByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId) { + return DaoUtil.convertDataList(widgetsBundleWidgetRepository.findAllByWidgetsBundleId(widgetsBundleId)); + } + + @Override + public void saveWidgetsBundleWidget(WidgetsBundleWidget widgetsBundleWidget) { + widgetsBundleWidgetRepository.save(new WidgetsBundleWidgetEntity(widgetsBundleWidget)); + } + + @Override + public void removeWidgetTypeFromWidgetsBundle(UUID widgetsBundleId, UUID widgetTypeId) { + widgetsBundleWidgetRepository.deleteById(new WidgetsBundleWidgetCompositeKey(widgetsBundleId, widgetTypeId)); + } + @Override public EntityType getEntityType() { return EntityType.WIDGET_TYPE; } + } 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 55e05fe2b4..41fa293dd5 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 @@ -15,37 +15,88 @@ */ package org.thingsboard.server.dao.sql.widget; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.WidgetTypeDetailsEntity; import org.thingsboard.server.dao.model.sql.WidgetTypeEntity; +import org.thingsboard.server.dao.model.sql.WidgetTypeIdFqnEntity; import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity; +import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity; import java.util.List; import java.util.UUID; -public interface WidgetTypeRepository extends JpaRepository { +public interface WidgetTypeRepository extends JpaRepository, ExportableEntityRepository { @Query("SELECT wt FROM WidgetTypeEntity wt WHERE wt.id = :widgetTypeId") WidgetTypeEntity findWidgetTypeById(@Param("widgetTypeId") UUID widgetTypeId); - @Query("SELECT wt FROM WidgetTypeEntity wt WHERE wt.tenantId = :tenantId AND wt.bundleAlias = :bundleAlias") - List findWidgetTypesByTenantIdAndBundleAlias(@Param("tenantId") UUID tenantId, - @Param("bundleAlias") String bundleAlias); + boolean existsByTenantIdAndId(UUID tenantId, UUID id); - @Query("SELECT new org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity(wtd) FROM WidgetTypeDetailsEntity wtd " + - "WHERE wtd.tenantId = :tenantId AND wtd.bundleAlias = :bundleAlias") - List findWidgetTypesInfosByTenantIdAndBundleAlias(@Param("tenantId") UUID tenantId, - @Param("bundleAlias") String bundleAlias); + @Query("SELECT new org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity(wtd) FROM WidgetTypeDetailsEntity wtd WHERE wtd.tenantId = :systemTenantId " + + "AND (LOWER(wtd.name) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + "OR ((:fullSearch) IS TRUE AND LOWER(wtd.description) LIKE LOWER(CONCAT('%', :searchText, '%'))))") + Page findSystemWidgetTypes(@Param("systemTenantId") UUID systemTenantId, + @Param("searchText") String searchText, + @Param("fullSearch") boolean fullSearch, + Pageable pageable); - List findByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias); + @Query("SELECT new org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity(wtd) FROM WidgetTypeDetailsEntity wtd WHERE wtd.tenantId IN (:tenantId, :nullTenantId) " + + "AND (LOWER(wtd.name) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + "OR ((:fullSearch) IS TRUE AND LOWER(wtd.description) LIKE LOWER(CONCAT('%', :searchText, '%'))))") + Page findAllTenantWidgetTypesByTenantId(@Param("tenantId") UUID tenantId, + @Param("nullTenantId") UUID nullTenantId, + @Param("searchText") String searchText, + @Param("fullSearch") boolean fullSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity(wtd) FROM WidgetTypeDetailsEntity wtd WHERE wtd.tenantId = :tenantId " + + "AND (LOWER(wtd.name) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + "OR ((:fullSearch) IS TRUE AND LOWER(wtd.description) LIKE LOWER(CONCAT('%', :searchText, '%'))))") + Page findTenantWidgetTypesByTenantId(@Param("tenantId") UUID tenantId, + @Param("searchText") String searchText, + @Param("fullSearch") boolean fullSearch, + Pageable pageable); + + @Query("SELECT wtd FROM WidgetTypeDetailsEntity wtd WHERE wtd.tenantId = :tenantId " + + "AND LOWER(wtd.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + Page findTenantWidgetTypeDetailsByTenantId(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT wt FROM WidgetTypeEntity wt, WidgetsBundleWidgetEntity wbw " + + "WHERE wbw.widgetsBundleId = :widgetsBundleId " + + "AND wbw.widgetTypeId = wt.id ORDER BY wbw.widgetTypeOrder") + List findWidgetTypesByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId); + + @Query("SELECT wtd FROM WidgetTypeDetailsEntity wtd, WidgetsBundleWidgetEntity wbw " + + "WHERE wbw.widgetsBundleId = :widgetsBundleId " + + "AND wbw.widgetTypeId = wtd.id ORDER BY wbw.widgetTypeOrder") + List findWidgetTypesDetailsByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity(wtd) FROM WidgetTypeDetailsEntity wtd, WidgetsBundleWidgetEntity wbw " + + "WHERE wbw.widgetsBundleId = :widgetsBundleId " + + "AND wbw.widgetTypeId = wtd.id ORDER BY wbw.widgetTypeOrder") + List findWidgetTypesInfosByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId); + + @Query("SELECT wtd.fqn FROM WidgetTypeDetailsEntity wtd, WidgetsBundleWidgetEntity wbw " + + "WHERE wbw.widgetsBundleId = :widgetsBundleId " + + "AND wbw.widgetTypeId = wtd.id ORDER BY wbw.widgetTypeOrder") + List findWidgetFqnsByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.WidgetTypeIdFqnEntity(wtd.id, wtd.fqn) FROM WidgetTypeDetailsEntity wtd " + + "WHERE wtd.tenantId = :tenantId " + + "AND wtd.fqn IN (:widgetFqns)") + List findWidgetTypeIdsByTenantIdAndFqns(@Param("tenantId") UUID tenantId, @Param("widgetFqns") List widgetFqns); @Query("SELECT wt FROM WidgetTypeEntity wt " + - "WHERE wt.tenantId = :tenantId AND wt.bundleAlias = :bundleAlias AND wt.alias = :alias") - WidgetTypeEntity findWidgetTypeByTenantIdAndBundleAliasAndAlias(@Param("tenantId") UUID tenantId, - @Param("bundleAlias") String bundleAlias, - @Param("alias") String alias); + "WHERE wt.tenantId = :tenantId AND wt.fqn = :fqn") + WidgetTypeEntity findWidgetTypeByTenantIdAndFqn(@Param("tenantId") UUID tenantId, + @Param("fqn") String fqn); @Query(value = "SELECT * FROM widget_type wt " + "WHERE wt.tenant_id = :tenantId AND cast(wt.descriptor as json) ->> 'resources' LIKE LOWER(CONCAT('%', :resourceId, '%'))", @@ -53,4 +104,7 @@ public interface WidgetTypeRepository extends JpaRepository findWidgetTypesInfosByTenantIdAndResourceId(@Param("tenantId") UUID tenantId, @Param("resourceId") UUID resourceId); + @Query("SELECT externalId FROM WidgetTypeDetailsEntity WHERE id = :id") + UUID getExternalIdById(@Param("id") UUID id); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleWidgetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleWidgetRepository.java new file mode 100644 index 0000000000..d064fca88e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleWidgetRepository.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.widget; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.thingsboard.server.dao.model.sql.WidgetsBundleWidgetCompositeKey; +import org.thingsboard.server.dao.model.sql.WidgetsBundleWidgetEntity; + +import java.util.List; +import java.util.UUID; + +public interface WidgetsBundleWidgetRepository extends JpaRepository { + + List findAllByWidgetsBundleId(UUID widgetsBundleId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index 4262aca2e4..3ff25972d2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -32,6 +32,8 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -92,6 +94,8 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService> futures, EntityId entityId, DeleteTsKvQuery query) { futures.add(Futures.transform(timeseriesDao.remove(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor())); - futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query)); + if (query.getDeleteLatest()) { + futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query)); + } } private static void validate(EntityId entityId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 5dc32b8eed..9776e5fe51 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -30,6 +30,7 @@ import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -44,6 +45,9 @@ import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityCountService; +import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -51,6 +55,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import static org.thingsboard.server.common.data.StringUtils.generateSafeToken; @@ -119,7 +124,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic } @Override - public User saveUser(User user) { + public User saveUser(TenantId tenantId, User user) { log.trace("Executing saveUser [{}]", user); userValidator.validate(user, User::getTenantId); if (!userLoginCaseSensitive) { @@ -135,6 +140,11 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode()); userCredentialsDao.save(user.getTenantId(), userCredentials); } + eventPublisher.publishEvent(SaveEntityEvent.builder() + .tenantId(tenantId == null ? TenantId.SYS_TENANT_ID : tenantId) + .entity(savedUser) + .entityId(savedUser.getId()) + .added(user.getId() == null).build()); return savedUser; } @@ -163,7 +173,12 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic public UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials) { log.trace("Executing saveUserCredentials [{}]", userCredentials); userCredentialsValidator.validate(userCredentials, data -> tenantId); - return userCredentialsDao.save(tenantId, userCredentials); + UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); + eventPublisher.publishEvent(ActionEntityEvent.builder() + .tenantId(tenantId) + .entityId(userCredentials.getUserId()) + .actionType(ActionType.CREDENTIALS_UPDATED).build()); + return result; } @Override @@ -222,13 +237,20 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic if (userCredentials.getPassword() != null) { updatePasswordHistory(userCredentials); } - return userCredentialsDao.save(tenantId, userCredentials); + UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); + eventPublisher.publishEvent(ActionEntityEvent.builder() + .tenantId(tenantId) + .entityId(userCredentials.getUserId()) + .actionType(ActionType.CREDENTIALS_UPDATED).build()); + return result; } @Override @Transactional - public void deleteUser(TenantId tenantId, UserId userId) { - log.trace("Executing deleteUser [{}]", userId); + public void deleteUser(TenantId tenantId, User user) { + Objects.requireNonNull(user, "User is null"); + UserId userId = user.getId(); + log.trace("[{}] Executing deleteUser [{}]", tenantId, userId); validateId(userId, INCORRECT_USER_ID + userId); UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, userId.getId()); userCredentialsDao.removeById(tenantId, userCredentials.getUuidId()); @@ -237,6 +259,10 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userDao.removeById(tenantId, userId.getId()); eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId)); countService.publishCountEntityEvictEvent(tenantId, EntityType.USER); + eventPublisher.publishEvent(DeleteEntityEvent.builder() + .tenantId(tenantId) + .entityId(userId) + .entity(user).build()); } @Override @@ -340,7 +366,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic log.trace("Executing onUserLoginSuccessful [{}]", userId); User user = findUserById(tenantId, userId); resetFailedLoginAttempts(user); - saveUser(user); + saveUser(tenantId, user); } private void resetFailedLoginAttempts(User user) { @@ -361,7 +387,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic } ((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis()); user.setAdditionalInfo(additionalInfo); - saveUser(user); + saveUser(tenantId, user); } @Override @@ -369,7 +395,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic log.trace("Executing onUserLoginIncorrectCredentials [{}]", userId); User user = findUserById(tenantId, userId); int failedLoginAttempts = increaseFailedLoginAttempts(user); - saveUser(user); + saveUser(tenantId, user); return failedLoginAttempts; } @@ -421,7 +447,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override protected void removeEntity(TenantId tenantId, User entity) { - deleteUser(tenantId, new UserId(entity.getUuidId())); + deleteUser(tenantId, entity); } }; @@ -434,7 +460,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override protected void removeEntity(TenantId tenantId, User entity) { - deleteUser(tenantId, new UserId(entity.getUuidId())); + deleteUser(tenantId, entity); } }; diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java index 4c8d0c5afe..d6c3a8efdd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java @@ -121,7 +121,7 @@ public abstract class AbstractBufferedRateExecutor taskCtx = null; @@ -169,7 +169,7 @@ public abstract class AbstractBufferedRateExecutor= printQueriesFreq) { printQueriesIdx.set(0); String query = queryToString(finalTaskCtx); - log.info("[{}] Cassandra query: {}", taskCtx.getId(), query); + log.info("[{}][{}] Cassandra query: {}", getBufferName(), taskCtx.getId(), query); } } logTask("Processing", finalTaskCtx); @@ -222,7 +222,7 @@ public abstract class AbstractBufferedRateExecutor taskCtx) { @@ -298,7 +298,7 @@ public abstract class AbstractBufferedRateExecutor tsKvEntries) { - tsKvEntries.forEach(KvUtils::validate); + public static void validate(List tsKvEntries, boolean valueNoXssValidation) { + tsKvEntries.forEach(tsKvEntry -> validate(tsKvEntry, valueNoXssValidation)); } - public static void validate(KvEntry tsKvEntry) { + public static void validate(KvEntry tsKvEntry, boolean valueNoXssValidation) { if (tsKvEntry == null) { throw new IncorrectParameterException("Key value entry can't be null"); } @@ -55,14 +56,20 @@ public class KvUtils { throw new DataValidationException("Validation error: key length must be equal or less than 255"); } - if (validatedKeys.getIfPresent(key) != null) { - return; + if (validatedKeys.getIfPresent(key) == null) { + if (!NoXssValidator.isValid(key)) { + throw new DataValidationException("Validation error: key is malformed"); + } + validatedKeys.put(key, Boolean.TRUE); } - if (!NoXssValidator.isValid(key)) { - throw new DataValidationException("Validation error: key is malformed"); + if (valueNoXssValidation) { + Object value = tsKvEntry.getValue(); + if (value instanceof CharSequence || value instanceof JsonNode) { + if (!NoXssValidator.isValid(value.toString())) { + throw new DataValidationException("Validation error: value is malformed"); + } + } } - - validatedKeys.put(key, Boolean.TRUE); } } 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 a54aa45b37..d9d19a9617 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 @@ -16,10 +16,15 @@ package org.thingsboard.server.dao.widget; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; +import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.ExportableEntityDao; import java.util.List; import java.util.UUID; @@ -27,7 +32,7 @@ import java.util.UUID; /** * The Interface WidgetTypeDao. */ -public interface WidgetTypeDao extends Dao { +public interface WidgetTypeDao extends Dao, ExportableEntityDao { /** * Save or update widget type object @@ -46,42 +51,51 @@ public interface WidgetTypeDao extends Dao { */ WidgetType findWidgetTypeById(TenantId tenantId, UUID widgetTypeId); + boolean existsByTenantIdAndId(TenantId tenantId, UUID widgetTypeId); + + PageData findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, PageLink pageLink); + + PageData findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink); + + PageData findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink); + /** - * Find widget types by tenantId and bundleAlias. + * Find widget types by widgetsBundleId. * * @param tenantId the tenantId - * @param bundleAlias the bundle alias + * @param widgetsBundleId the widgets bundle id * @return the list of widget types objects */ - List findWidgetTypesByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias); + List findWidgetTypesByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId); /** - * Find widget types details by tenantId and bundleAlias. + * Find widget types details by widgetsBundleId. * * @param tenantId the tenantId - * @param bundleAlias the bundle alias + * @param widgetsBundleId the widgets bundle id * @return the list of widget types details objects */ - List findWidgetTypesDetailsByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias); + List findWidgetTypesDetailsByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId); /** - * Find widget types infos by tenantId and bundleAlias. + * Find widget types infos by widgetsBundleId. * * @param tenantId the tenantId - * @param bundleAlias the bundle alias + * @param widgetsBundleId the widgets bundle id * @return the list of widget types infos objects */ - List findWidgetTypesInfosByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias); + List findWidgetTypesInfosByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId); + + List findWidgetFqnsByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId); /** - * Find widget type by tenantId, bundleAlias and alias. + * Find widget type by tenantId and FQN. * * @param tenantId the tenantId - * @param bundleAlias the bundle alias - * @param alias the alias + * @param fqn the FQN * @return the widget type object */ - WidgetType findByTenantIdBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias); + WidgetType findByTenantIdAndFqn(UUID tenantId, String fqn); /** * Find widget types infos by tenantId and resourceId in descriptor. @@ -91,4 +105,13 @@ public interface WidgetTypeDao extends Dao { * @return the list of widget types infos objects */ List findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId); + + List findWidgetTypeIdsByTenantIdAndFqns(UUID tenantId, List widgetFqns); + + List findWidgetsBundleWidgetsByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId); + + void saveWidgetsBundleWidget(WidgetsBundleWidget widgetsBundleWidget); + + void removeWidgetTypeFromWidgetsBundle(UUID widgetsBundleId, UUID widgetTypeId); + } 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 1ac099b075..410e0e4a3f 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 @@ -17,21 +17,33 @@ package org.thingsboard.server.dao.widget; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; -import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; +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.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; +import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; +import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; + +import static org.thingsboard.server.dao.service.Validator.validateIds; @Service("WidgetTypeDaoService") @Slf4j @@ -40,12 +52,17 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; public static final String INCORRECT_BUNDLE_ALIAS = "Incorrect bundleAlias "; + public static final String INCORRECT_WIDGETS_BUNDLE_ID = "Incorrect widgetsBundleId "; + @Autowired private WidgetTypeDao widgetTypeDao; @Autowired private DataValidator widgetTypeValidator; + @Autowired + protected ApplicationEventPublisher eventPublisher; + @Override public WidgetType findWidgetTypeById(TenantId tenantId, WidgetTypeId widgetTypeId) { log.trace("Executing findWidgetTypeById [{}]", widgetTypeId); @@ -60,11 +77,28 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { return widgetTypeDao.findById(tenantId, widgetTypeId.getId()); } + @Override + public boolean widgetTypeExistsByTenantIdAndWidgetTypeId(TenantId tenantId, WidgetTypeId widgetTypeId) { + log.trace("Executing widgetTypeExistsByTenantIdAndWidgetTypeId, tenantId [{}], widgetTypeId [{}]", tenantId, widgetTypeId); + Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId); + return widgetTypeDao.existsByTenantIdAndId(tenantId, widgetTypeId.getId()); + } + @Override public WidgetTypeDetails saveWidgetType(WidgetTypeDetails widgetTypeDetails) { log.trace("Executing saveWidgetType [{}]", widgetTypeDetails); widgetTypeValidator.validate(widgetTypeDetails, WidgetType::getTenantId); - return widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails); + try { + WidgetTypeDetails result = widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getTenantId()) + .entityId(result.getId()).added(widgetTypeDetails.getId() == null).build()); + return result; + } catch (Exception t) { + AbstractCachedEntityService.checkConstraintViolation(t, + "uq_widget_type_fqn", "Widget type with such fqn already exists!"); + AbstractCachedEntityService.checkConstraintViolation(t, "widget_type_external_id_unq_key", "Widget type with such external id already exists!"); + throw t; + } } @Override @@ -72,58 +106,110 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { log.trace("Executing deleteWidgetType [{}]", widgetTypeId); Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId); widgetTypeDao.removeById(tenantId, widgetTypeId.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(widgetTypeId).build()); } @Override - public List findWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias) { - log.trace("Executing findWidgetTypesByTenantIdAndBundleAlias, tenantId [{}], bundleAlias [{}]", tenantId, bundleAlias); + public PageData findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) { + log.trace("Executing findSystemWidgetTypesByPageLink, fullSearch [{}] pageLink [{}]", fullSearch, pageLink); + Validator.validatePageLink(pageLink); + return widgetTypeDao.findSystemWidgetTypes(tenantId, fullSearch, pageLink); + } + + @Override + public PageData findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) { + log.trace("Executing findAllTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink); + Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validatePageLink(pageLink); + return widgetTypeDao.findAllTenantWidgetTypesByTenantId(tenantId.getId(), fullSearch, pageLink); + } + + @Override + public PageData findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) { + log.trace("Executing findTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateString(bundleAlias, INCORRECT_BUNDLE_ALIAS + bundleAlias); - return widgetTypeDao.findWidgetTypesByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias); + Validator.validatePageLink(pageLink); + return widgetTypeDao.findTenantWidgetTypesByTenantId(tenantId.getId(), fullSearch, pageLink); } @Override - public List findWidgetTypesDetailsByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias) { - log.trace("Executing findWidgetTypesDetailsByTenantIdAndBundleAlias, tenantId [{}], bundleAlias [{}]", tenantId, bundleAlias); + public List findWidgetTypesByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId) { + log.trace("Executing findWidgetTypesByWidgetsBundleId, tenantId [{}], widgetsBundleId [{}]", tenantId, widgetsBundleId); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateString(bundleAlias, INCORRECT_BUNDLE_ALIAS + bundleAlias); - return widgetTypeDao.findWidgetTypesDetailsByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias); + Validator.validateId(widgetsBundleId, INCORRECT_WIDGETS_BUNDLE_ID + widgetsBundleId); + return widgetTypeDao.findWidgetTypesByWidgetsBundleId(tenantId.getId(), widgetsBundleId.getId()); } @Override - public List findWidgetTypesInfosByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias) { - log.trace("Executing findWidgetTypesInfosByTenantIdAndBundleAlias, tenantId [{}], bundleAlias [{}]", tenantId, bundleAlias); + public List findWidgetTypesDetailsByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId) { + log.trace("Executing findWidgetTypesDetailsByWidgetsBundleId, tenantId [{}], widgetsBundleId [{}]", tenantId, widgetsBundleId); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateString(bundleAlias, INCORRECT_BUNDLE_ALIAS + bundleAlias); - return widgetTypeDao.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias); + Validator.validateId(widgetsBundleId, INCORRECT_WIDGETS_BUNDLE_ID + widgetsBundleId); + return widgetTypeDao.findWidgetTypesDetailsByWidgetsBundleId(tenantId.getId(), widgetsBundleId.getId()); + } @Override - public List findWidgetTypesInfosByTenantIdAndResourceId(TenantId tenantId, TbResourceId tbResourceId) { - log.trace("Executing findWidgetTypesInfosByTenantIdAndResourceId, tenantId [{}], tbResourceId [{}]", tenantId, tbResourceId); + public List findWidgetTypesInfosByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId) { + log.trace("Executing findWidgetTypesInfosByWidgetsBundleId, tenantId [{}], widgetsBundleId [{}]", tenantId, widgetsBundleId); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateId(tbResourceId, INCORRECT_RESOURCE_ID + tbResourceId); - return widgetTypeDao.findWidgetTypesInfosByTenantIdAndResourceId(tenantId.getId(), tbResourceId.getId()); + Validator.validateId(widgetsBundleId, INCORRECT_WIDGETS_BUNDLE_ID + widgetsBundleId); + return widgetTypeDao.findWidgetTypesInfosByWidgetsBundleId(tenantId.getId(), widgetsBundleId.getId()); } @Override - public WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias) { - log.trace("Executing findWidgetTypeByTenantIdBundleAliasAndAlias, tenantId [{}], bundleAlias [{}], alias [{}]", tenantId, bundleAlias, alias); + public List findWidgetFqnsByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId) { + log.trace("Executing findWidgetTypesInfosByWidgetsBundleId, tenantId [{}], widgetsBundleId [{}]", tenantId, widgetsBundleId); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateString(bundleAlias, INCORRECT_BUNDLE_ALIAS + bundleAlias); - Validator.validateString(alias, "Incorrect alias " + alias); - return widgetTypeDao.findByTenantIdBundleAliasAndAlias(tenantId.getId(), bundleAlias, alias); + Validator.validateId(widgetsBundleId, INCORRECT_WIDGETS_BUNDLE_ID + widgetsBundleId); + return widgetTypeDao.findWidgetFqnsByWidgetsBundleId(tenantId.getId(), widgetsBundleId.getId()); } @Override - public void deleteWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias) { - log.trace("Executing deleteWidgetTypesByTenantIdAndBundleAlias, tenantId [{}], bundleAlias [{}]", tenantId, bundleAlias); + public WidgetType findWidgetTypeByTenantIdAndFqn(TenantId tenantId, String fqn) { + log.trace("Executing findWidgetTypeByTenantIdAndFqn, tenantId [{}], fqn [{}]", tenantId, fqn); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateString(bundleAlias, INCORRECT_BUNDLE_ALIAS + bundleAlias); - List widgetTypes = widgetTypeDao.findWidgetTypesByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias); - for (WidgetType widgetType : widgetTypes) { - deleteWidgetType(tenantId, new WidgetTypeId(widgetType.getUuidId())); + Validator.validateString(fqn, "Incorrect fqn " + fqn); + return widgetTypeDao.findByTenantIdAndFqn(tenantId.getId(), fqn); + } + + @Override + public void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List widgetTypeIds) { + log.trace("Executing updateWidgetsBundleWidgetTypes, tenantId [{}], widgetsBundleId [{}], widgetTypeIds [{}]", tenantId, widgetsBundleId, widgetTypeIds); + Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(widgetsBundleId, INCORRECT_WIDGETS_BUNDLE_ID + widgetsBundleId); + validateIds(widgetTypeIds, "Incorrect widgetTypeIds " + widgetTypeIds); + List bundleWidgets = new ArrayList<>(); + for (int index = 0; index < widgetTypeIds.size(); index++) { + bundleWidgets.add(new WidgetsBundleWidget(widgetsBundleId, widgetTypeIds.get(index), index)); } + List existingBundleWidgets = widgetTypeDao.findWidgetsBundleWidgetsByWidgetsBundleId(tenantId.getId(), widgetsBundleId.getId()); + List toRemove = existingBundleWidgets.stream() + .map(WidgetsBundleWidget::getWidgetTypeId) + .filter(widgetTypeId -> bundleWidgets.stream().noneMatch(newBundleWidget -> + newBundleWidget.getWidgetTypeId().equals(widgetTypeId))).collect(Collectors.toList()); + for (WidgetTypeId widgetTypeId : toRemove) { + widgetTypeDao.removeWidgetTypeFromWidgetsBundle(widgetsBundleId.getId(), widgetTypeId.getId()); + } + for (WidgetsBundleWidget widgetsBundleWidget : bundleWidgets) { + widgetTypeDao.saveWidgetsBundleWidget(widgetsBundleWidget); + } + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId) + .entityId(widgetsBundleId).added(false).build()); + } + + @Override + public void updateWidgetsBundleWidgetFqns(TenantId tenantId, WidgetsBundleId widgetsBundleId, List widgetFqns) { + log.trace("Executing updateWidgetsBundleWidgetFqns, tenantId [{}], widgetsBundleId [{}], widgetFqns [{}]", tenantId, widgetsBundleId, widgetFqns); + List widgetTypeIds = widgetTypeDao.findWidgetTypeIdsByTenantIdAndFqns(tenantId.getId(), widgetFqns); + this.updateWidgetsBundleWidgetTypes(tenantId, widgetsBundleId, widgetTypeIds); + } + + @Override + public void deleteWidgetTypesByTenantId(TenantId tenantId) { + log.trace("Executing deleteWidgetTypesByTenantId, tenantId [{}]", tenantId); + Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + tenantWidgetTypeRemover.removeEntities(tenantId, tenantId); } @Override @@ -136,4 +222,18 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { return EntityType.WIDGET_TYPE; } + private PaginatedRemover tenantWidgetTypeRemover = + new PaginatedRemover<>() { + + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return widgetTypeDao.findTenantWidgetTypesByTenantId(id.getId(), false, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, WidgetTypeInfo entity) { + deleteWidgetType(tenantId, new WidgetTypeId(entity.getUuidId())); + } + }; + } 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 63a4ce0a77..4f094ca62f 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 @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.widget; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; @@ -27,6 +28,8 @@ 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.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -53,6 +56,9 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Autowired private DataValidator widgetsBundleValidator; + @Autowired + protected ApplicationEventPublisher eventPublisher; + @Override public WidgetsBundle findWidgetsBundleById(TenantId tenantId, WidgetsBundleId widgetsBundleId) { log.trace("Executing findWidgetsBundleById [{}]", widgetsBundleId); @@ -65,9 +71,14 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { log.trace("Executing saveWidgetsBundle [{}]", widgetsBundle); widgetsBundleValidator.validate(widgetsBundle, WidgetsBundle::getTenantId); try { - return widgetsBundleDao.save(widgetsBundle.getTenantId(), widgetsBundle); + WidgetsBundle result = widgetsBundleDao.save(widgetsBundle.getTenantId(), widgetsBundle); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getTenantId()) + .entityId(result.getId()).added(widgetsBundle.getId() == null).build()); + return result; } catch (Exception e) { - AbstractCachedEntityService.checkConstraintViolation(e, "widgets_bundle_external_id_unq_key", "Widget Bundle with such external id already exists!"); + AbstractCachedEntityService.checkConstraintViolation(e, + "uq_widgets_bundle_alias", "Widgets Bundle with such alias already exists!"); + AbstractCachedEntityService.checkConstraintViolation(e, "widgets_bundle_external_id_unq_key", "Widgets Bundle with such external id already exists!"); throw e; } } @@ -80,7 +91,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { if (widgetsBundle == null) { throw new IncorrectParameterException("Unable to delete non-existent widgets bundle."); } - widgetTypeService.deleteWidgetTypesByTenantIdAndBundleAlias(widgetsBundle.getTenantId(), widgetsBundle.getAlias()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(widgetsBundleId).build()); widgetsBundleDao.removeById(tenantId, widgetsBundleId.getId()); } diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 12e0bfddba..675fcd3ec0 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -113,3 +113,5 @@ CREATE INDEX IF NOT EXISTS idx_notification_request_status ON notification_reque CREATE INDEX IF NOT EXISTS idx_notification_id ON notification(id); CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notification(recipient_id, created_time DESC); + +CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ'; diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index bfb2eed805..883e9da4a4 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -25,7 +25,7 @@ CREATE OR REPLACE PROCEDURE insert_tb_schema_settings() $$ BEGIN IF (SELECT COUNT(*) FROM tb_schema_settings) = 0 THEN - INSERT INTO tb_schema_settings (schema_version) VALUES (3003000); + INSERT INTO tb_schema_settings (schema_version) VALUES (3005002); END IF; END; $$; @@ -482,13 +482,16 @@ CREATE TABLE IF NOT EXISTS user_credentials ( CREATE TABLE IF NOT EXISTS widget_type ( id uuid NOT NULL CONSTRAINT widget_type_pkey PRIMARY KEY, created_time bigint NOT NULL, - alias varchar(255), - bundle_alias varchar(255), + fqn varchar(512), descriptor varchar(1000000), name varchar(255), tenant_id uuid, image varchar(1000000), - description varchar(255) + deprecated boolean NOT NULL DEFAULT false, + description varchar(255), + external_id uuid, + CONSTRAINT uq_widget_type_fqn UNIQUE (tenant_id, fqn), + CONSTRAINT widget_type_external_id_unq_key UNIQUE (tenant_id, external_id) ); CREATE TABLE IF NOT EXISTS widgets_bundle ( @@ -500,9 +503,19 @@ CREATE TABLE IF NOT EXISTS widgets_bundle ( image varchar(1000000), description varchar(255), external_id uuid, + CONSTRAINT uq_widgets_bundle_alias UNIQUE (tenant_id, alias), CONSTRAINT widgets_bundle_external_id_unq_key UNIQUE (tenant_id, external_id) ); +CREATE TABLE IF NOT EXISTS widgets_bundle_widget ( + widgets_bundle_id uuid NOT NULL, + widget_type_id uuid NOT NULL, + widget_type_order int NOT NULL DEFAULT 0, + CONSTRAINT widgets_bundle_widget_pkey PRIMARY KEY (widgets_bundle_id, widget_type_id), + CONSTRAINT fk_widgets_bundle FOREIGN KEY (widgets_bundle_id) REFERENCES widgets_bundle(id) ON DELETE CASCADE, + CONSTRAINT fk_widget_type FOREIGN KEY (widget_type_id) REFERENCES widget_type(id) ON DELETE CASCADE +); + CREATE TABLE IF NOT EXISTS entity_view ( id uuid NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY, created_time bigint NOT NULL, @@ -826,7 +839,7 @@ CREATE TABLE IF NOT EXISTS notification_request ( targets VARCHAR(10000) NOT NULL, template_id UUID, template VARCHAR(10000000), - info VARCHAR(1000), + info VARCHAR(1000000), additional_config VARCHAR(1000), originator_entity_id UUID, originator_entity_type VARCHAR(32), @@ -854,3 +867,10 @@ CREATE TABLE IF NOT EXISTS user_settings ( CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE, CONSTRAINT user_settings_pkey PRIMARY KEY (user_id, type) ); + +CREATE TABLE IF NOT EXISTS alarm_types ( + tenant_id uuid NOT NULL, + type varchar(255) NOT NULL, + CONSTRAINT tenant_id_type_unq_key UNIQUE (tenant_id, type), + CONSTRAINT fk_entity_tenant_id FOREIGN KEY (tenant_id) REFERENCES tenant(id) ON DELETE CASCADE +); diff --git a/dao/src/main/resources/sql/schema-views-and-functions.sql b/dao/src/main/resources/sql/schema-views-and-functions.sql index 3cd20ac0c4..3aa797d7b6 100644 --- a/dao/src/main/resources/sql/schema-views-and-functions.sql +++ b/dao/src/main/resources/sql/schema-views-and-functions.sql @@ -114,6 +114,7 @@ BEGIN a_details, a_propagate, a_propagate_to_owner, a_propagate_to_tenant, a_propagation_types, false, 0, false, 0, NULL, 0); + INSERT INTO alarm_types (tenant_id, type) VALUES (t_id, a_type) ON CONFLICT (tenant_id, type) DO NOTHING; SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; RETURN json_build_object('success', true, 'created', true, 'modified', true, 'alarm', row_to_json(result))::text; ELSE diff --git a/dao/src/test/java/org/thingsboard/server/dao/eventsourcing/DeleteEntityEventTest.java b/dao/src/test/java/org/thingsboard/server/dao/eventsourcing/DeleteEntityEventTest.java new file mode 100644 index 0000000000..39d228ceac --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/eventsourcing/DeleteEntityEventTest.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.eventsourcing; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.BDDAssertions.byLessThan; + +class DeleteEntityEventTest { + + @Test + void testBuilderDefaultTs() { + assertThat(DeleteEntityEvent.builder().build().getTs()) + .isCloseTo(System.currentTimeMillis(), byLessThan(TimeUnit.MINUTES.toMillis(1))); + + assertThat(DeleteEntityEvent.builder().ts(Long.MIN_VALUE).build().getTs()) + .isEqualTo(Long.MIN_VALUE); + assertThat(DeleteEntityEvent.builder().ts(Long.MAX_VALUE).build().getTs()) + .isEqualTo(Long.MAX_VALUE); + assertThat(DeleteEntityEvent.builder().ts(-1L).build().getTs()) + .isEqualTo(-1L); + assertThat(DeleteEntityEvent.builder().ts(0L).build().getTs()) + .isEqualTo(0L); + + assertThat(DeleteEntityEvent.builder().ts(1692175215000L).build().getTs()) + .isEqualTo(1692175215000L); + } + +} \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index e609b93289..91c6488447 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -59,6 +59,7 @@ import java.nio.ByteBuffer; import java.util.Comparator; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.UUID; import static org.junit.Assert.assertNotNull; @@ -80,7 +81,7 @@ public abstract class AbstractServiceTest { @Before public void beforeAbstractService() { - tenantId = createTenant(); + tenantId = createTenant().getId(); } @After @@ -107,29 +108,10 @@ public abstract class AbstractServiceTest { .data(JacksonUtil.toString(readFromResource("TestJsonData.json"))) .build(); } -// -// private ComponentDescriptor getOrCreateDescriptor(ComponentScope scope, ComponentType type, String clazz, String configurationDescriptorResource) throws IOException { -// return getOrCreateDescriptor(scope, type, clazz, configurationDescriptorResource, null); -// } -// -// private ComponentDescriptor getOrCreateDescriptor(ComponentScope scope, ComponentType type, String clazz, String configurationDescriptorResource, String actions) throws IOException { -// ComponentDescriptor descriptor = componentDescriptorService.findByClazz(clazz); -// if (descriptor == null) { -// descriptor = new ComponentDescriptor(); -// descriptor.setName("test"); -// descriptor.setClazz(clazz); -// descriptor.setScope(scope); -// descriptor.setType(type); -// descriptor.setActions(actions); -// descriptor.setConfigurationDescriptor(readFromResource(configurationDescriptorResource)); -// componentDescriptorService.saveComponent(descriptor); -// } -// return descriptor; -// } public JsonNode readFromResource(String resourceName) throws IOException { try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourceName)){ - return JacksonUtil.fromBytes(is.readAllBytes()); + return JacksonUtil.fromBytes(Objects.requireNonNull(is).readAllBytes()); } } @@ -172,12 +154,12 @@ public abstract class AbstractServiceTest { return assetProfile; } - public TenantId createTenant() { + public Tenant createTenant() { Tenant tenant = new Tenant(); tenant.setTitle("My tenant " + UUID.randomUUID()); Tenant savedTenant = tenantService.saveTenant(tenant); assertNotNull(savedTenant); - return savedTenant.getId(); + return savedTenant; } protected Edge constructEdge(TenantId tenantId, String name, String type) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmCommentServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmCommentServiceTest.java index 22257242f5..8aa3eeb125 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmCommentServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmCommentServiceTest.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -71,7 +72,7 @@ public class AlarmCommentServiceTest extends AbstractServiceTest { user.setEmail("tenant@thingsboard.org"); user.setFirstName("John"); user.setLastName("Brown"); - user = userService.saveUser(user); + user = userService.saveUser(TenantId.SYS_TENANT_ID, user); } @After diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java index 5b0981b44a..412056855e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; @@ -358,7 +359,7 @@ public class AlarmServiceTest extends AbstractServiceTest { tenantUser.setEmail(TEST_TENANT_EMAIL); tenantUser.setFirstName(TEST_TENANT_FIRST_NAME); tenantUser.setLastName(TEST_TENANT_LAST_NAME); - tenantUser = userService.saveUser(tenantUser); + tenantUser = userService.saveUser(TenantId.SYS_TENANT_ID, tenantUser); Assert.assertNotNull(tenantUser); @@ -392,7 +393,7 @@ public class AlarmServiceTest extends AbstractServiceTest { tenantUser2.setEmail(2 + TEST_TENANT_EMAIL); tenantUser2.setFirstName(TEST_TENANT_FIRST_NAME); tenantUser2.setLastName(TEST_TENANT_LAST_NAME); - tenantUser2 = userService.saveUser(tenantUser2); + tenantUser2 = userService.saveUser(TenantId.SYS_TENANT_ID, tenantUser2); Assert.assertNotNull(tenantUser2); pageLink.setAssigneeId(tenantUser2.getId()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index adb97196fd..ccddcb16bf 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -85,6 +85,17 @@ public class AssetServiceTest extends AbstractServiceTest { }); } + @Test + public void testSaveDeviceWithNameContains0x00_thenDataValidationException() { + Asset asset = new Asset(); + asset.setTenantId(tenantId); + asset.setType("default"); + asset.setName("F0929906\000\000\000\000\000\000\000\000\000"); + Assertions.assertThrows(DataValidationException.class, () -> { + assetService.saveAsset(asset); + }); + } + @Test public void testSaveAssetWithEmptyTenant() { Asset asset = new Asset(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java index 40262ac2b8..0eafd9aeaf 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java @@ -15,62 +15,93 @@ */ package org.thingsboard.server.dao.service; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.thingsboard.server.dao.exception.DataValidationException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; + +@Slf4j public class DataValidatorTest { - @Test - public void validateEmail() { - String email = "aZ1_!#$%&'*+/=?`{|}~^.-@mail.io"; - DataValidator.validateEmail(email); + DataValidator dataValidator; + + @BeforeEach + void setUp() { + dataValidator = spy(DataValidator.class); } - @Test - public void validateInvalidEmail1() { - String email = "test:1@mail.io"; - Assertions.assertThrows(DataValidationException.class, () -> { - DataValidator.validateEmail(email); - }); + @ParameterizedTest + @ValueSource(strings = { + "coffee", "1", "big box", "世界", "!", "--", "~!@#$%^&*()_+=-/|\\[]{};:'`\"?<>,.", "\uD83D\uDC0C", "\041", + "Gdy Pomorze nie pomoże, to pomoże może morze, a gdy morze nie pomoże, to pomoże może Gdańsk", + }) + void validateName_thenOK(final String name) { + dataValidator.validateString("Device name", name); + dataValidator.validateString("Asset name", name); + dataValidator.validateString("Asset profile name", name); + dataValidator.validateString("Alarm type", name); + dataValidator.validateString("Customer name", name); + dataValidator.validateString("Tenant name", name); } - @Test - public void validateInvalidEmail2() { - String email = "test()1@mail.io"; - Assertions.assertThrows(DataValidationException.class, () -> { - DataValidator.validateEmail(email); - }); + + @ParameterizedTest + @ValueSource(strings = { + "", " ", " ", "\n", "\r\n", "\t", "\000", "\000\000", "\001", "\002", "\040", "\u0000", "\u0000\u0000", + "F0929906\000\000\000\000\000\000\000\000\000", "\000\000\000F0929906", + "\u0000F0929906", "F092\u00009906", "F0929906\u0000" + }) + void validateName_thenDataValidationException(final String name) { + DataValidationException exception; + exception = Assertions.assertThrows(DataValidationException.class, () -> dataValidator.validateString("Asset name", name)); + log.warn("Exception message Asset name: {}", exception.getMessage()); + assertThat(exception.getMessage()).as("message Asset name").containsPattern("Asset name .*"); + + exception = Assertions.assertThrows(DataValidationException.class, () -> dataValidator.validateString("Device name", name)); + log.warn("Exception message Device name: {}", exception.getMessage()); + assertThat(exception.getMessage()).as("message Device name").containsPattern("Device name .*"); } - @Test - public void validateInvalidEmail3() { - String email = "test[]1@mail.io"; - Assertions.assertThrows(DataValidationException.class, () -> { - DataValidator.validateEmail(email); - }); + @ParameterizedTest + @ValueSource(strings = { + "aZ1_!#$%&'*+/=?`{|}~^.-@mail.io", "support@thingsboard.io", + }) + public void validateEmail(String email) { + DataValidator.validateEmail(email); } - @Test - public void validateInvalidEmail4() { - String email = "test\\1@mail.io"; - Assertions.assertThrows(DataValidationException.class, () -> { - DataValidator.validateEmail(email); - }); + @ParameterizedTest + @ValueSource(strings = { + "test:1@mail.io", "test()1@mail.io", "test[]1@mail.io", + "test\\1@mail.io", "test\"1@mail.io", "test<>1@mail.io", + }) + public void validateEmailInvalid(String email) { + Assertions.assertThrows(DataValidationException.class, () -> DataValidator.validateEmail(email)); } - @Test - public void validateInvalidEmail5() { - String email = "test\"1@mail.io"; - Assertions.assertThrows(DataValidationException.class, () -> { - DataValidator.validateEmail(email); - }); + @ParameterizedTest + @ValueSource(strings = { + "azAZ09_.-", "topic", + }) + public void validateQueueNameOrTopic(String value) { + DataValidator.validateQueueNameOrTopic(value, "name"); + DataValidator.validateQueueNameOrTopic(value, "topic"); } - @Test - public void validateInvalidEmail6() { - String email = "test<>1@mail.io"; - Assertions.assertThrows(DataValidationException.class, () -> { - DataValidator.validateEmail(email); - }); + @ParameterizedTest + @ValueSource(strings = { + "", " ", " ", "\n", "\r\n", "\t", "\000", "\000\000", "\001", "\002", "\040", "\u0000", "\u0000\u0000", + "topic@home", "!", ",", "Łódź", + "\uD83D\uDC0C", "\041", + "F0929906\000\000\000\000\000\000\000\000\000", + }) + public void validateQueueNameOrTopicInvalid(String value) { + Assertions.assertThrows(DataValidationException.class, () -> DataValidator.validateQueueNameOrTopic(value, "name")); + Assertions.assertThrows(DataValidationException.class, () -> DataValidator.validateQueueNameOrTopic(value, "topic")); } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index b19af16015..1073f20be4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -78,7 +78,7 @@ public class DeviceServiceTest extends AbstractServiceTest { @Before public void before() { - anotherTenantId = createTenant(); + anotherTenantId = createTenant().getId(); } @After @@ -283,6 +283,17 @@ public class DeviceServiceTest extends AbstractServiceTest { }); } + @Test + public void testSaveDeviceWithNameContains0x00_thenDataValidationException() { + Device device = new Device(); + device.setType("default"); + device.setTenantId(tenantId); + device.setName("F0929906\000\000\000\000\000\000\000\000\000"); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceService.saveDevice(device); + }); + } + @Test public void testSaveDeviceWithInvalidTenant() { Device device = new Device(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java index adeb543625..65a5797c08 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java @@ -650,4 +650,23 @@ public class EdgeServiceTest extends AbstractServiceTest { Assert.assertEquals("{\"Rule Chain #3\":[\"Rule Chain #1\",\"Rule Chain #2\"]}", missingToRelatedRuleChains); } + @Test + public void testFindEdgesByTenantProfileId() { + Tenant tenant1 = createTenant(); + Tenant tenant2 = createTenant(); + Assert.assertNotNull(tenant1); + Assert.assertNotNull(tenant2); + + Edge edge1 = constructEdge(tenant1.getId(), "Tenant1 edge", "default"); + Edge edge2 = constructEdge(tenant2.getId(), "Tenant2 edge", "default"); + Edge savedEdge1 = edgeService.saveEdge(edge1); + Edge savedEdge2 = edgeService.saveEdge(edge2); + Assert.assertNotNull(savedEdge1); + Assert.assertNotNull(savedEdge2); + Assert.assertEquals(tenant1.getTenantProfileId(), tenant2.getTenantProfileId()); + + PageData edgesPageData = edgeService.findEdgesByTenantProfileId(tenant2.getTenantProfileId(), + new PageLink(1000)); + Assert.assertEquals(2, edgesPageData.getTotalElements()); + } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java index 22d6bfcd9c..f41e92a305 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java @@ -260,7 +260,7 @@ public class EntityServiceTest extends AbstractServiceTest { user.setAuthority(Authority.TENANT_ADMIN); user.setEmail(StringUtils.randomAlphabetic(10) + "@gmail.com"); user.setPhone(StringUtils.randomNumeric(10)); - user = userService.saveUser(user); + user = userService.saveUser(tenantId, user); users.add(user); createRelation(tenantId, "Contains", tenantId, user.getId()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java index 3bbb71306b..148f969d48 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java @@ -131,6 +131,31 @@ public class RelationServiceTest extends AbstractServiceTest { Assert.assertFalse(relationService.checkRelation(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)); } + @Test + public void testDeleteEntityCommonRelations() { + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); + AssetId subChildId = new AssetId(Uuids.timeBased()); + + EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE); + EntityRelation relationC = new EntityRelation(parentId, childId, EntityRelation.MANAGES_TYPE, RelationTypeGroup.EDGE); + EntityRelation relationD = new EntityRelation(childId, subChildId, EntityRelation.MANAGES_TYPE, RelationTypeGroup.EDGE); + + saveRelation(relationA); + saveRelation(relationB); + saveRelation(relationC); + saveRelation(relationD); + + relationService.deleteEntityCommonRelations(SYSTEM_TENANT_ID, childId); + + Assert.assertFalse(relationService.checkRelation(SYSTEM_TENANT_ID, parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)); + Assert.assertFalse(relationService.checkRelation(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)); + + Assert.assertTrue(relationService.checkRelation(SYSTEM_TENANT_ID, parentId, childId, EntityRelation.MANAGES_TYPE, RelationTypeGroup.EDGE)); + Assert.assertTrue(relationService.checkRelation(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.MANAGES_TYPE, RelationTypeGroup.EDGE)); + } + @Test public void testFindFrom() throws ExecutionException, InterruptedException { AssetId parentA = new AssetId(Uuids.timeBased()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/TenantProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/TenantProfileServiceTest.java index 51f8aabb57..94d3b8f98e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/TenantProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/TenantProfileServiceTest.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.FSTUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; @@ -45,6 +46,9 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + @DaoSqlTest public class TenantProfileServiceTest extends AbstractServiceTest { @@ -188,17 +192,6 @@ public class TenantProfileServiceTest extends AbstractServiceTest { }); } - @Test - public void testSaveSameTenantProfileWithDifferentIsolatedTbRuleEngine() { - TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); - TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); - savedTenantProfile.setIsolatedTbRuleEngine(true); - addMainQueueConfig(savedTenantProfile); - Assertions.assertThrows(DataValidationException.class, () -> { - tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile); - }); - } - @Test public void testDeleteTenantProfileWithExistingTenant() { TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); @@ -306,6 +299,29 @@ public class TenantProfileServiceTest extends AbstractServiceTest { } + @Test + public void testTenantProfileSerialization_fst() { + TenantProfile tenantProfile = new TenantProfile(); + TenantProfileData profileData = new TenantProfileData(); + tenantProfile.setProfileData(profileData); + profileData.setConfiguration(new DefaultTenantProfileConfiguration()); + addMainQueueConfig(tenantProfile); + + byte[] serialized = assertDoesNotThrow(() -> { + return FSTUtils.encode(tenantProfile); + }); + assertDoesNotThrow(() -> { + FSTUtils.encode(profileData); + }); + + TenantProfile deserialized = assertDoesNotThrow(() -> { + return FSTUtils.decode(serialized); + }); + assertThat(deserialized).isEqualTo(tenantProfile); + assertThat(deserialized.getProfileData()).isNotNull(); + assertThat(deserialized.getProfileData().getQueueConfiguration()).isNotEmpty(); + } + public static TenantProfile createTenantProfile(String name) { TenantProfile tenantProfile = new TenantProfile(); tenantProfile.setName(name); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java index 5872a5846b..ce1a83514a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java @@ -716,7 +716,7 @@ public class TenantServiceTest extends AbstractServiceTest { user.setFirstName("tenantAdmin"); user.setLastName("tenantAdmin"); user.setTenantId(tenant.getId()); - return userService.saveUser(user); + return userService.saveUser(TenantId.SYS_TENANT_ID, user); } private Tenant createAndSaveTenant(TenantProfile tenantProfile) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java index fcd57e5b3a..711d3ed805 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java @@ -58,7 +58,7 @@ public class UserServiceTest extends AbstractServiceTest { tenantAdmin.setAuthority(Authority.TENANT_ADMIN); tenantAdmin.setTenantId(tenantId); tenantAdmin.setEmail("tenant@thingsboard.org"); - userService.saveUser(tenantAdmin); + userService.saveUser(TenantId.SYS_TENANT_ID, tenantAdmin); Customer customer = new Customer(); customer.setTenantId(tenantId); @@ -70,7 +70,7 @@ public class UserServiceTest extends AbstractServiceTest { customerUser.setTenantId(tenantId); customerUser.setCustomerId(savedCustomer.getId()); customerUser.setEmail("customer@thingsboard.org"); - customerUser = userService.saveUser(customerUser); + customerUser = userService.saveUser(tenantId, customerUser); userSettings = createUserSettings(customerUser.getId()); } @@ -114,7 +114,7 @@ public class UserServiceTest extends AbstractServiceTest { user.setAuthority(Authority.TENANT_ADMIN); user.setTenantId(tenantAdminUser.getTenantId()); user.setEmail("tenant2@thingsboard.org"); - User savedUser = userService.saveUser(user); + User savedUser = userService.saveUser(TenantId.SYS_TENANT_ID, user); Assert.assertNotNull(savedUser); Assert.assertNotNull(savedUser.getId()); Assert.assertTrue(savedUser.getCreatedTime() > 0); @@ -130,12 +130,12 @@ public class UserServiceTest extends AbstractServiceTest { savedUser.setFirstName("Joe"); savedUser.setLastName("Downs"); - userService.saveUser(savedUser); + userService.saveUser(TenantId.SYS_TENANT_ID, savedUser); savedUser = userService.findUserById(tenantId, savedUser.getId()); Assert.assertEquals("Joe", savedUser.getFirstName()); Assert.assertEquals("Downs", savedUser.getLastName()); - userService.deleteUser(tenantId, savedUser.getId()); + userService.deleteUser(tenantId, savedUser); } @Test @@ -143,7 +143,7 @@ public class UserServiceTest extends AbstractServiceTest { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); tenantAdminUser.setEmail("sysadmin@thingsboard.org"); Assertions.assertThrows(DataValidationException.class, () -> { - userService.saveUser(tenantAdminUser); + userService.saveUser(tenantId, tenantAdminUser); }); } @@ -152,7 +152,7 @@ public class UserServiceTest extends AbstractServiceTest { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); tenantAdminUser.setEmail("tenant_thingsboard.org"); Assertions.assertThrows(DataValidationException.class, () -> { - userService.saveUser(tenantAdminUser); + userService.saveUser(tenantId, tenantAdminUser); }); } @@ -161,7 +161,7 @@ public class UserServiceTest extends AbstractServiceTest { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); tenantAdminUser.setEmail(null); Assertions.assertThrows(DataValidationException.class, () -> { - userService.saveUser(tenantAdminUser); + userService.saveUser(tenantId, tenantAdminUser); }); } @@ -170,7 +170,7 @@ public class UserServiceTest extends AbstractServiceTest { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); tenantAdminUser.setTenantId(null); Assertions.assertThrows(DataValidationException.class, () -> { - userService.saveUser(tenantAdminUser); + userService.saveUser(tenantId, tenantAdminUser); }); } @@ -181,14 +181,14 @@ public class UserServiceTest extends AbstractServiceTest { user.setAuthority(Authority.TENANT_ADMIN); user.setTenantId(tenantAdminUser.getTenantId()); user.setEmail("tenant2@thingsboard.org"); - User savedUser = userService.saveUser(user); + User savedUser = userService.saveUser(TenantId.SYS_TENANT_ID, user); Assert.assertNotNull(savedUser); Assert.assertNotNull(savedUser.getId()); User foundUser = userService.findUserById(tenantId, savedUser.getId()); Assert.assertNotNull(foundUser); UserCredentials userCredentials = userService.findUserCredentialsByUserId(tenantId, foundUser.getId()); Assert.assertNotNull(userCredentials); - userService.deleteUser(tenantId, foundUser.getId()); + userService.deleteUser(tenantId, foundUser); userCredentials = userService.findUserCredentialsByUserId(tenantId, foundUser.getId()); foundUser = userService.findUserById(tenantId, foundUser.getId()); Assert.assertNull(foundUser); @@ -204,7 +204,7 @@ public class UserServiceTest extends AbstractServiceTest { Assert.assertEquals(1, users.size()); Assert.assertEquals(tenantAdminUser, users.get(0)); - TenantId secondTenantId = createTenant(); + TenantId secondTenantId = createTenant().getId(); List tenantAdmins = new ArrayList<>(); for (int i = 0; i < 124; i++) { @@ -212,7 +212,7 @@ public class UserServiceTest extends AbstractServiceTest { user.setAuthority(Authority.TENANT_ADMIN); user.setTenantId(secondTenantId); user.setEmail("testTenant" + i + "@thingsboard.org"); - tenantAdmins.add(userService.saveUser(user)); + tenantAdmins.add(userService.saveUser(TenantId.SYS_TENANT_ID, user)); } List loadedTenantAdmins = new ArrayList<>(); @@ -252,7 +252,7 @@ public class UserServiceTest extends AbstractServiceTest { String email = email1 + suffix + "@thingsboard.org"; email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase(); user.setEmail(email); - tenantAdminsEmail1.add(userService.saveUser(user)); + tenantAdminsEmail1.add(userService.saveUser(TenantId.SYS_TENANT_ID, user)); } String email2 = "testEmail2"; @@ -266,7 +266,7 @@ public class UserServiceTest extends AbstractServiceTest { String email = email2 + suffix + "@thingsboard.org"; email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase(); user.setEmail(email); - tenantAdminsEmail2.add(userService.saveUser(user)); + tenantAdminsEmail2.add(userService.saveUser(TenantId.SYS_TENANT_ID, user)); } List loadedTenantAdminsEmail1 = new ArrayList<>(); @@ -301,7 +301,7 @@ public class UserServiceTest extends AbstractServiceTest { Assert.assertEquals(tenantAdminsEmail2, loadedTenantAdminsEmail2); for (User user : loadedTenantAdminsEmail1) { - userService.deleteUser(tenantId, user.getId()); + userService.deleteUser(tenantId, user); } pageLink = new PageLink(4, 0, email1); @@ -310,7 +310,7 @@ public class UserServiceTest extends AbstractServiceTest { Assert.assertEquals(0, pageData.getData().size()); for (User user : loadedTenantAdminsEmail2) { - userService.deleteUser(tenantId, user.getId()); + userService.deleteUser(tenantId, user); } pageLink = new PageLink(4, 0, email2); @@ -343,7 +343,7 @@ public class UserServiceTest extends AbstractServiceTest { user.setTenantId(tenantId); user.setCustomerId(customerId); user.setEmail("testCustomer" + i + "@thingsboard.org"); - customerUsers.add(userService.saveUser(user)); + customerUsers.add(userService.saveUser(tenantId, user)); } List loadedCustomerUsers = new ArrayList<>(); @@ -390,7 +390,7 @@ public class UserServiceTest extends AbstractServiceTest { String email = email1 + suffix + "@thingsboard.org"; email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase(); user.setEmail(email); - customerUsersEmail1.add(userService.saveUser(user)); + customerUsersEmail1.add(userService.saveUser(tenantId, user)); } String email2 = "testEmail2"; @@ -405,7 +405,7 @@ public class UserServiceTest extends AbstractServiceTest { String email = email2 + suffix + "@thingsboard.org"; email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase(); user.setEmail(email); - customerUsersEmail2.add(userService.saveUser(user)); + customerUsersEmail2.add(userService.saveUser(tenantId, user)); } List loadedCustomerUsersEmail1 = new ArrayList<>(); @@ -440,7 +440,7 @@ public class UserServiceTest extends AbstractServiceTest { Assert.assertEquals(customerUsersEmail2, loadedCustomerUsersEmail2); for (User user : loadedCustomerUsersEmail1) { - userService.deleteUser(tenantId, user.getId()); + userService.deleteUser(tenantId, user); } pageLink = new PageLink(4, 0, email1); @@ -449,7 +449,7 @@ public class UserServiceTest extends AbstractServiceTest { Assert.assertEquals(0, pageData.getData().size()); for (User user : loadedCustomerUsersEmail2) { - userService.deleteUser(tenantId, user.getId()); + userService.deleteUser(tenantId, user); } pageLink = new PageLink(4, 0, email2); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceTest.java index 9a2b5e6180..679c288ae7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceTest.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; @@ -35,12 +36,14 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; @DaoSqlTest public class WidgetTypeServiceTest extends AbstractServiceTest { @Autowired WidgetsBundleService widgetsBundleService; + @Autowired WidgetTypeService widgetTypeService; @@ -48,27 +51,19 @@ public class WidgetTypeServiceTest extends AbstractServiceTest { @Test public void testSaveWidgetType() throws IOException { - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTenantId(tenantId); - widgetsBundle.setTitle("Widgets bundle"); - WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); - - WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType); Assert.assertNotNull(savedWidgetType); Assert.assertNotNull(savedWidgetType.getId()); - Assert.assertNotNull(savedWidgetType.getAlias()); + Assert.assertNotNull(savedWidgetType.getFqn()); Assert.assertTrue(savedWidgetType.getCreatedTime() > 0); Assert.assertEquals(widgetType.getTenantId(), savedWidgetType.getTenantId()); Assert.assertEquals(widgetType.getName(), savedWidgetType.getName()); Assert.assertEquals(widgetType.getDescriptor(), savedWidgetType.getDescriptor()); - Assert.assertEquals(savedWidgetsBundle.getAlias(), savedWidgetType.getBundleAlias()); savedWidgetType.setName("New Widget Type"); @@ -76,34 +71,13 @@ public class WidgetTypeServiceTest extends AbstractServiceTest { WidgetType foundWidgetType = widgetTypeService.findWidgetTypeById(tenantId, savedWidgetType.getId()); Assert.assertEquals(foundWidgetType.getName(), savedWidgetType.getName()); - widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); + widgetTypeService.deleteWidgetType(tenantId, savedWidgetType.getId()); } @Test public void testSaveWidgetTypeWithEmptyName() throws IOException { - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTenantId(tenantId); - widgetsBundle.setTitle("Widgets bundle"); - WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); - WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); - widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); - try { - Assertions.assertThrows(DataValidationException.class, () -> { - widgetTypeService.saveWidgetType(widgetType); - }); - } finally { - widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); - } - } - - @Test - public void testSaveWidgetTypeWithEmptyBundleAlias() throws IOException { - WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setTenantId(tenantId); - widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); Assertions.assertThrows(DataValidationException.class, () -> { widgetTypeService.saveWidgetType(widgetType); @@ -112,51 +86,19 @@ public class WidgetTypeServiceTest extends AbstractServiceTest { @Test public void testSaveWidgetTypeWithEmptyDescriptor() throws IOException { - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTenantId(tenantId); - widgetsBundle.setTitle("Widgets bundle"); - WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); - WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); widgetType.setName("Widget Type"); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setDescriptor(JacksonUtil.fromString("{}", JsonNode.class)); - try { - Assertions.assertThrows(DataValidationException.class, () -> { - widgetTypeService.saveWidgetType(widgetType); - }); - } finally { - widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); - } + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(widgetType); + }); } @Test public void testSaveWidgetTypeWithInvalidTenant() throws IOException { - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTenantId(tenantId); - widgetsBundle.setTitle("Widgets bundle"); - WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); - WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); - widgetType.setName("Widget Type"); - widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); - try { - Assertions.assertThrows(DataValidationException.class, () -> { - widgetTypeService.saveWidgetType(widgetType); - }); - } finally { - widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); - } - } - - @Test - public void testSaveWidgetTypeWithInvalidBundleAlias() throws IOException { - WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setTenantId(tenantId); - widgetType.setBundleAlias("some_alias"); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); Assertions.assertThrows(DataValidationException.class, () -> { @@ -166,14 +108,8 @@ public class WidgetTypeServiceTest extends AbstractServiceTest { @Test public void testUpdateWidgetTypeTenant() throws IOException { - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTenantId(tenantId); - widgetsBundle.setTitle("Widgets bundle"); - WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); - WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType); @@ -183,106 +119,57 @@ public class WidgetTypeServiceTest extends AbstractServiceTest { widgetTypeService.saveWidgetType(savedWidgetType); }); } finally { - widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); + widgetTypeService.deleteWidgetType(tenantId, savedWidgetType.getId()); } } @Test - public void testUpdateWidgetTypeBundleAlias() throws IOException { - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTenantId(tenantId); - widgetsBundle.setTitle("Widgets bundle"); - WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); - + public void testUpdateWidgetTypeFqn() throws IOException { WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType); - savedWidgetType.setBundleAlias("some_alias"); + savedWidgetType.setFqn("some_fqn"); try { Assertions.assertThrows(DataValidationException.class, () -> { widgetTypeService.saveWidgetType(savedWidgetType); }); } finally { - widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); - } - } - - @Test - public void testUpdateWidgetTypeAlias() throws IOException { - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTenantId(tenantId); - widgetsBundle.setTitle("Widgets bundle"); - WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); - - WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setTenantId(tenantId); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); - widgetType.setName("Widget Type"); - widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); - WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType); - savedWidgetType.setAlias("some_alias"); - try { - Assertions.assertThrows(DataValidationException.class, () -> { - widgetTypeService.saveWidgetType(savedWidgetType); - }); - } finally { - widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); + widgetTypeService.deleteWidgetType(tenantId, savedWidgetType.getId()); } } @Test public void testFindWidgetTypeById() throws IOException { - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTenantId(tenantId); - widgetsBundle.setTitle("Widgets bundle"); - WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); - WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType); WidgetTypeDetails foundWidgetType = widgetTypeService.findWidgetTypeDetailsById(tenantId, savedWidgetType.getId()); Assert.assertNotNull(foundWidgetType); Assert.assertEquals(savedWidgetType, foundWidgetType); - - widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); + widgetTypeService.deleteWidgetType(tenantId, savedWidgetType.getId()); } @Test - public void testFindWidgetTypeByTenantIdBundleAliasAndAlias() throws IOException { - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTenantId(tenantId); - widgetsBundle.setTitle("Widgets bundle"); - WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); - + public void testFindWidgetTypeByTenantIdAndFqn() throws IOException { WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetType savedWidgetType = new WidgetType(widgetTypeService.saveWidgetType(widgetType)); - WidgetType foundWidgetType = widgetTypeService.findWidgetTypeByTenantIdBundleAliasAndAlias(tenantId, savedWidgetsBundle.getAlias(), savedWidgetType.getAlias()); + WidgetType foundWidgetType = widgetTypeService.findWidgetTypeByTenantIdAndFqn(tenantId, savedWidgetType.getFqn()); Assert.assertNotNull(foundWidgetType); Assert.assertEquals(savedWidgetType, foundWidgetType); - - widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); + widgetTypeService.deleteWidgetType(tenantId, savedWidgetType.getId()); } @Test public void testDeleteWidgetType() throws IOException { - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTenantId(tenantId); - widgetsBundle.setTitle("Widgets bundle"); - WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); - WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType); @@ -291,12 +178,10 @@ public class WidgetTypeServiceTest extends AbstractServiceTest { widgetTypeService.deleteWidgetType(tenantId, savedWidgetType.getId()); foundWidgetType = widgetTypeService.findWidgetTypeById(tenantId, savedWidgetType.getId()); Assert.assertNull(foundWidgetType); - - widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } @Test - public void testFindWidgetTypesByTenantIdAndBundleAlias() throws IOException { + public void testFindWidgetTypesByTenantIdAndWidgetsBundleId() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); widgetsBundle.setTitle("Widgets bundle"); @@ -306,22 +191,27 @@ public class WidgetTypeServiceTest extends AbstractServiceTest { for (int i=0;i<121;i++) { WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); - widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type " + i); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); widgetTypes.add(new WidgetType(widgetTypeService.saveWidgetType(widgetType))); } - List loadedWidgetTypes = widgetTypeService.findWidgetTypesByTenantIdAndBundleAlias(tenantId, savedWidgetsBundle.getAlias()); + List widgetTypeIds = widgetTypes.stream().map(WidgetType::getId).collect(Collectors.toList()); + + widgetTypeService.updateWidgetsBundleWidgetTypes(tenantId, savedWidgetsBundle.getId(), widgetTypeIds); + + List loadedWidgetTypes = widgetTypeService.findWidgetTypesByWidgetsBundleId(tenantId, savedWidgetsBundle.getId()); Collections.sort(widgetTypes, idComparator); Collections.sort(loadedWidgetTypes, idComparator); Assert.assertEquals(widgetTypes, loadedWidgetTypes); - widgetTypeService.deleteWidgetTypesByTenantIdAndBundleAlias(tenantId, savedWidgetsBundle.getAlias()); + for (WidgetTypeId id : widgetTypeIds) { + widgetTypeService.deleteWidgetType(tenantId, id); + } - loadedWidgetTypes = widgetTypeService.findWidgetTypesByTenantIdAndBundleAlias(tenantId, savedWidgetsBundle.getAlias()); + loadedWidgetTypes = widgetTypeService.findWidgetTypesByWidgetsBundleId(tenantId, savedWidgetsBundle.getId()); Assert.assertTrue(loadedWidgetTypes.isEmpty()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidatorTest.java new file mode 100644 index 0000000000..1f021b16e7 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidatorTest.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.settings.AdminSettingsService; + +import java.util.UUID; + +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = AdminSettingsDataValidator.class) +class AdminSettingsDataValidatorTest { + + @MockBean + AdminSettingsService adminSettingsService; + @SpyBean + AdminSettingsDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @Test + void testValidateNameInvocation() { + AdminSettings adminSettings = new AdminSettings(); + adminSettings.setKey("jwt"); + adminSettings.setJsonValue(JacksonUtil.toJsonNode("{}")); + + validator.validateDataImpl(tenantId, adminSettings); + verify(validator).validateString("Key", adminSettings.getKey()); + } + +} \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/AlarmDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AlarmDataValidatorTest.java new file mode 100644 index 0000000000..fc91a099fa --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AlarmDataValidatorTest.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = AlarmDataValidator.class) +class AlarmDataValidatorTest { + + @MockBean + TenantService tenantService; + @SpyBean + AlarmDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @Test + void testValidateNameInvocation() { + Alarm alarm = new Alarm(); + alarm.setType("overheating"); + alarm.setOriginator(tenantId); + alarm.setSeverity(AlarmSeverity.CRITICAL); + alarm.setCleared(false); + alarm.setAcknowledged(false); + alarm.setTenantId(tenantId); + + validator.validateDataImpl(tenantId, alarm); + verify(validator).validateString("Alarm type", alarm.getType()); + } + +} \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetDataValidatorTest.java new file mode 100644 index 0000000000..7922cdacfa --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetDataValidatorTest.java @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.asset.AssetDao; +import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.willReturn; + +@SpringBootTest(classes = AssetDataValidator.class) +@Slf4j +class AssetDataValidatorTest { + + @MockBean + AssetDao assetDao; + @MockBean + TenantService tenantService; + @MockBean + CustomerDao customerDao; + @Autowired + AssetDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @ParameterizedTest + @ValueSource(strings = { + "coffee", "1", "big box", "世界", "!", "--", "~!@#$%^&*()_+=-/|\\[]{};:'`\"?<>,.", "\uD83D\uDC0C", "\041", + "Gdy Pomorze nie pomoże, to pomoże może morze, a gdy morze nie pomoże, to pomoże może Gdańsk", + }) + void testAssetName_thenOK(final String name) { + Asset asset = new Asset(); + asset.setTenantId(tenantId); + asset.setName(name); + validator.validateDataImpl(tenantId, asset); + } + + @ParameterizedTest + @ValueSource(strings = { + "", " ", " ", "\n", "\r\n", "\t", "\000", "\000\000", "\001", "\002", "\040", "\u0000", "\u0000\u0000", + "F0929906\000\000\000\000\000\000\000\000\000", "\000\000\000F0929906", + "\u0000F0929906", "F092\u00009906", "F0929906\u0000" + }) + void testAssetName_thenDataValidationException(final String name) { + Asset asset = new Asset(); + asset.setTenantId(tenantId); + asset.setName(name); + + DataValidationException exception = Assertions.assertThrows(DataValidationException.class, () -> validator.validateDataImpl(tenantId, asset)); + log.warn("Exception message: {}", exception.getMessage()); + assertThat(exception.getMessage()).as("message Asset name").containsPattern("Asset name .*"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidatorTest.java new file mode 100644 index 0000000000..66fdd54c86 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidatorTest.java @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.asset.AssetProfileDao; +import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = AssetProfileDataValidator.class) +class AssetProfileDataValidatorTest { + + @MockBean + AssetProfileDao assetProfileDao; + @MockBean + AssetProfileService assetProfileService; + @MockBean + TenantService tenantService; + @MockBean + QueueService queueService; + @MockBean + RuleChainService ruleChainService; + @MockBean + DashboardService dashboardService; + @SpyBean + AssetProfileDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @Test + void testValidateNameInvocation() { + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setName("prod"); + assetProfile.setTenantId(tenantId); + + validator.validateDataImpl(tenantId, assetProfile); + verify(validator).validateString("Asset profile name", assetProfile.getName()); + } + +} \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidatorTest.java new file mode 100644 index 0000000000..74fac9cfb9 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidatorTest.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.ota.OtaPackageType; +import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +class BaseOtaPackageDataValidatorTest { + + + DeviceProfileDao deviceProfileDao = mock(DeviceProfileDao.class); + TenantService tenantService = mock(TenantService.class); + BaseOtaPackageDataValidator validator = spy(BaseOtaPackageDataValidator.class); + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + willReturn(tenantService).given(validator).getTenantService(); + willReturn(deviceProfileDao).given(validator).getDeviceProfileDao(); + } + + @Test + void testValidateNameInvocation() { + OtaPackageInfo otaPackageInfo = new OtaPackageInfo(); + otaPackageInfo.setTitle("fw"); + otaPackageInfo.setVersion("1.0"); + otaPackageInfo.setType(OtaPackageType.FIRMWARE); + otaPackageInfo.setTenantId(tenantId); + + validator.validateImpl(otaPackageInfo); + verify(validator).validateString("OtaPackage title", otaPackageInfo.getTitle()); + } + + +} \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidatorTest.java new file mode 100644 index 0000000000..7effbf57ba --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidatorTest.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentDescriptor; +import org.thingsboard.server.common.data.plugin.ComponentScope; +import org.thingsboard.server.common.data.plugin.ComponentType; + +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = ComponentDescriptorDataValidator.class) +class ComponentDescriptorDataValidatorTest { + @SpyBean + ComponentDescriptorDataValidator validator; + + @Test + void testValidateNameInvocation() { + ComponentDescriptor plugin = new ComponentDescriptor(); + plugin.setType(ComponentType.ENRICHMENT); + plugin.setScope(ComponentScope.SYSTEM); + plugin.setName("originator attributes"); + plugin.setClazz("org.thingsboard.rule.engine.metadata.TbGetAttributesNode"); + validator.validateDataImpl(TenantId.SYS_TENANT_ID, plugin); + verify(validator).validateString("Component name", plugin.getName()); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CustomerDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CustomerDataValidatorTest.java new file mode 100644 index 0000000000..67329ba864 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CustomerDataValidatorTest.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = CustomerDataValidator.class) +class CustomerDataValidatorTest { + + @MockBean + CustomerDao customerDao; + @MockBean + TenantService tenantService; + @SpyBean + CustomerDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @Test + void testValidateNameInvocation() { + Customer customer = new Customer(); + customer.setTitle("Customer A"); + customer.setTenantId(tenantId); + + validator.validateDataImpl(tenantId, customer); + verify(validator).validateString("Customer title", customer.getTitle()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DashboardDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DashboardDataValidatorTest.java new file mode 100644 index 0000000000..b2953ad912 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DashboardDataValidatorTest.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = DashboardDataValidator.class) +class DashboardDataValidatorTest { + + @MockBean + TenantService tenantService; + @SpyBean + DashboardDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @Test + void testValidateNameInvocation() { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("flight control"); + dashboard.setTenantId(tenantId); + + validator.validateDataImpl(tenantId, dashboard); + verify(validator).validateString("Dashboard title", dashboard.getTitle()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceDataValidatorTest.java new file mode 100644 index 0000000000..4c4ed5a0e2 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceDataValidatorTest.java @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.device.DeviceDao; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.willReturn; + +@SpringBootTest(classes = DeviceDataValidator.class) +@Slf4j +class DeviceDataValidatorTest { + + @MockBean + DeviceDao deviceDao; + @MockBean + TenantService tenantService; + @MockBean + CustomerDao customerDao; + @Autowired + DeviceDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @ParameterizedTest + @ValueSource(strings = { + "coffee", "1", "big box", "世界", "!", "--", "~!@#$%^&*()_+=-/|\\[]{};:'`\"?<>,.", "\uD83D\uDC0C", "\041", + "Gdy Pomorze nie pomoże, to pomoże może morze, a gdy morze nie pomoże, to pomoże może Gdańsk", + }) + void testDeviceName_thenOK(final String name) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName(name); + validator.validateDataImpl(tenantId, device); + } + + @ParameterizedTest + @ValueSource(strings = { + "", " ", " ", "\n", "\r\n", "\t", "\000", "\000\000", "\001", "\002", "\040", "\u0000", "\u0000\u0000", + "F0929906\000\000\000\000\000\000\000\000\000", "\000\000\000F0929906", + "\u0000F0929906", "F092\u00009906", "F0929906\u0000" + }) + void testDeviceName_thenDataValidationException(final String name) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName(name); + + DataValidationException exception = Assertions.assertThrows(DataValidationException.class, () -> validator.validateDataImpl(tenantId, device)); + log.warn("Exception message: {}", exception.getMessage()); + assertThat(exception.getMessage()).as("message Device name").containsPattern("Device name .*"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java new file mode 100644 index 0000000000..fc066c201d --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.device.DeviceDao; +import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = DeviceProfileDataValidator.class) +class DeviceProfileDataValidatorTest { + + @MockBean + DeviceProfileDao deviceProfileDao; + @MockBean + DeviceProfileService deviceProfileService; + @MockBean + DeviceDao deviceDao; + @MockBean + TenantService tenantService; + @MockBean + QueueService queueService; + @MockBean + RuleChainService ruleChainService; + @MockBean + DashboardService dashboardService; + @SpyBean + DeviceProfileDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @Test + void testValidateNameInvocation() { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setName("default"); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + DeviceProfileData data = new DeviceProfileData(); + data.setTransportConfiguration(new DefaultDeviceProfileTransportConfiguration()); + deviceProfile.setProfileData(data); + deviceProfile.setTenantId(tenantId); + + validator.validateDataImpl(tenantId, deviceProfile); + verify(validator).validateString("Device profile name", deviceProfile.getName()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/EdgeDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/EdgeDataValidatorTest.java new file mode 100644 index 0000000000..d852b3680a --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/EdgeDataValidatorTest.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.edge.EdgeDao; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = EdgeDataValidator.class) +class EdgeDataValidatorTest { + + @MockBean + EdgeDao edgeDao; + @MockBean + TenantService tenantService; + @MockBean + CustomerDao customerDao; + @SpyBean + EdgeDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @Test + void testValidateNameInvocation() { + Edge edge = new Edge(); + edge.setName("Edge 007"); + edge.setType("Silos"); + edge.setSecret("secret"); + edge.setRoutingKey("53c56104-d302-4d6e-97f5-a7a99c7effdc"); + edge.setTenantId(tenantId); + + validator.validateDataImpl(tenantId, edge); + verify(validator).validateString("Edge name", edge.getName()); + verify(validator).validateString("Edge type", edge.getType()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidatorTest.java new file mode 100644 index 0000000000..0eebb15554 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidatorTest.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.entityview.EntityViewDao; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = EntityViewDataValidator.class) +class EntityViewDataValidatorTest { + + @MockBean + EntityViewDao entityViewDao; + @MockBean + TenantService tenantService; + @MockBean + CustomerDao customerDao; + @SpyBean + EntityViewDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @Test + void testValidateNameInvocation() { + EntityView entityView = new EntityView(); + entityView.setName("view"); + entityView.setType("default"); + entityView.setTenantId(tenantId); + + validator.validateDataImpl(tenantId, entityView); + verify(validator).validateString("Entity view name", entityView.getName()); + verify(validator).validateString("Entity view type", entityView.getType()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/ResourceDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/ResourceDataValidatorTest.java new file mode 100644 index 0000000000..985b80331f --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/ResourceDataValidatorTest.java @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.resource.TbResourceDao; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.widget.WidgetTypeDao; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = ResourceDataValidator.class) +class ResourceDataValidatorTest { + + @MockBean + TbResourceDao resourceDao; + @MockBean + WidgetTypeDao widgetTypeDao; + @MockBean + TenantService tenantService; + @MockBean + TbTenantProfileCache tenantProfileCache; + @SpyBean + ResourceDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @Test + void testValidateNameInvocation() { + TbResource resource = new TbResource(); + resource.setTitle("rss"); + resource.setResourceType(ResourceType.PKCS_12); + resource.setFileName("cert.pem"); + resource.setResourceKey("19_1.0"); + resource.setTenantId(tenantId); + + validator.validateDataImpl(tenantId, resource); + verify(validator).validateString("Resource title", resource.getTitle()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidatorTest.java new file mode 100644 index 0000000000..7f9d3e1fc2 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidatorTest.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.dao.rule.RuleChainDao; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = RuleChainDataValidator.class) +class RuleChainDataValidatorTest { + + @MockBean + RuleChainDao ruleChainDao; + @MockBean + RuleChainService ruleChainService; + @MockBean + TenantService tenantService; + @SpyBean + RuleChainDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @Test + void testValidateNameInvocation() { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName("generate daily report"); + ruleChain.setType(RuleChainType.CORE); + ruleChain.setTenantId(tenantId); + + validator.validateDataImpl(tenantId, ruleChain); + verify(validator).validateString("Rule chain name", ruleChain.getName()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantDataValidatorTest.java new file mode 100644 index 0000000000..e6c6a96146 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantDataValidatorTest.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.tenant.TenantDao; + +import java.util.UUID; + +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = TenantDataValidator.class) +class TenantDataValidatorTest { + + @MockBean + TenantDao tenantDao; + @SpyBean + TenantDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @Test + void testValidateNameInvocation() { + Tenant tenant = new Tenant(); + tenant.setTitle("Monster corporation ©"); + tenant.setEmail("support@thingsboard.io"); + + validator.validateDataImpl(tenantId, tenant); + verify(validator).validateString("Tenant title", tenant.getTitle()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java new file mode 100644 index 0000000000..e3bca21984 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.dao.tenant.TenantProfileDao; +import org.thingsboard.server.dao.tenant.TenantProfileService; + +import java.util.UUID; + +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = TenantProfileDataValidator.class) +class TenantProfileDataValidatorTest { + + @MockBean + TenantProfileDao tenantProfileDao; + @MockBean + TenantProfileService tenantProfileService; + @SpyBean + TenantProfileDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @Test + void testValidateNameInvocation() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Sandbox"); + TenantProfileData tenantProfileData = new TenantProfileData(); + tenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration()); + tenantProfile.setProfileData(tenantProfileData); + + validator.validateDataImpl(tenantId, tenantProfile); + verify(validator).validateString("Tenant profile name", tenantProfile.getName()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidatorTest.java new file mode 100644 index 0000000000..209d2b4bb8 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidatorTest.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.widget.WidgetTypeDao; +import org.thingsboard.server.dao.widget.WidgetsBundleDao; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = WidgetTypeDataValidator.class) +class WidgetTypeDataValidatorTest { + @MockBean + WidgetTypeDao widgetTypeDao; + @MockBean + WidgetsBundleDao widgetsBundleDao; + @MockBean + TenantService tenantService; + @SpyBean + WidgetTypeDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @Test + void testValidateNameInvocation() { + WidgetTypeDetails widgetTypeDetails = new WidgetTypeDetails(); + widgetTypeDetails.setName("widget type gas"); + widgetTypeDetails.setDescriptor(JacksonUtil.toJsonNode("{\"content\":\"empty\"}")); + widgetTypeDetails.setTenantId(tenantId); + + validator.validateDataImpl(tenantId, widgetTypeDetails); + verify(validator).validateString("Widgets type name", widgetTypeDetails.getName()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidatorTest.java new file mode 100644 index 0000000000..9e54f85689 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidatorTest.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.widget.WidgetsBundleDao; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +@SpringBootTest(classes = WidgetsBundleDataValidator.class) +class WidgetsBundleDataValidatorTest { + + @MockBean + WidgetsBundleDao widgetsBundleDao; + @MockBean + TenantService tenantService; + @SpyBean + WidgetsBundleDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + @Test + void testValidateNameInvocation() { + WidgetsBundle widgetsBundle = new WidgetsBundle(); + widgetsBundle.setTitle("my fancy WB"); + widgetsBundle.setTenantId(tenantId); + + validator.validateDataImpl(tenantId, widgetsBundle); + verify(validator).validateString("Widgets bundle title", widgetsBundle.getTitle()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java index 25231f822f..8f0122aa4d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java @@ -20,6 +20,8 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; @@ -29,8 +31,12 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.alarm.AlarmDao; +import org.thingsboard.server.dao.tenant.TenantDao; +import org.thingsboard.server.dao.tenant.TenantProfileDao; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -54,6 +60,11 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { @Autowired private AlarmDao alarmDao; + @Autowired + protected TenantProfileDao tenantProfileDao; + + @Autowired + protected TenantDao tenantDao; @Test public void testFindLatestByOriginatorAndType() throws ExecutionException, InterruptedException, TimeoutException { @@ -88,7 +99,8 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { @Test public void createOrUpdateActiveAlarm() { - TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + Tenant tenant = createTenant(); + TenantId tenantId = tenant.getId(); DeviceId deviceId = new DeviceId(UUID.randomUUID()); AlarmCreateOrUpdateActiveRequest request = AlarmCreateOrUpdateActiveRequest.builder() @@ -156,6 +168,9 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { assertTrue(result.isModified()); assertNotNull(result.getAlarm()); assertNotEquals(newAlarmId, result.getAlarm().getUuidId()); + + tenantDao.removeById(TenantId.SYS_TENANT_ID, tenant.getUuidId()); + tenantProfileDao.removeById(TenantId.SYS_TENANT_ID, tenant.getTenantProfileId().getId()); } @Test @@ -329,4 +344,23 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { return alarmDao.save(TenantId.fromUUID(tenantId), alarm); } + private Tenant createTenant() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("My tenant profile " + UUID.randomUUID()); + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(new DefaultTenantProfileConfiguration()); + tenantProfile.setProfileData(profileData); + var savedTenantProfile = tenantProfileDao.save(TenantId.SYS_TENANT_ID, tenantProfile); + assertNotNull(savedTenantProfile); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant " + UUID.randomUUID()); + tenant.setTenantProfileId(savedTenantProfile.getId()); + Tenant savedTenant = tenantDao.save(TenantId.SYS_TENANT_ID, tenant); + + assertNotNull(savedTenant); + + return savedTenant; + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java index ef68465256..1ffb740553 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java @@ -44,6 +44,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -94,6 +95,12 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { savedAssetProfiles.clear(); } + @Test + public void testSaveDeviceName0x00_thenSomeDatabaseException() { + assertThatThrownBy(() -> assets.add( + saveAsset(UUID.randomUUID(), tenantId2, customerId2, "F0929906\000\000\000\000\000\000\000\000\000"))); + } + @Test public void testFindAssetsByTenantId() { PageLink pageLink = new PageLink(20, 0, "ASSET_"); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java index dc174b7423..c8e6c2cfc5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java @@ -45,6 +45,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -101,6 +102,12 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { } } + @Test + public void testSaveDeviceName0x00_thenSomeDatabaseException() { + Device device = getDevice(tenantId1, customerId1, "\u0000"); + assertThatThrownBy(() -> deviceIds.add(deviceDao.save(TenantId.fromUUID(tenantId1), device).getUuidId())); + } + @Test public void testFindDevicesByTenantId() { PageLink pageLink = new PageLink(15, 0, PREFIX_FOR_DEVICE_NAME); @@ -155,16 +162,16 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { return savedDevicesUUID; } - private Device getDevice(UUID tenantId, UUID customerID, int number) { - return getDevice(tenantId, customerID, Uuids.timeBased(), number); + private Device getDevice(UUID tenantId, UUID customerID, Object nameSuffix) { + return getDevice(tenantId, customerID, Uuids.timeBased(), nameSuffix); } - private Device getDevice(UUID tenantId, UUID customerID, UUID deviceId, int number) { + private Device getDevice(UUID tenantId, UUID customerID, UUID deviceId, Object nameSuffix) { Device device = new Device(); device.setId(new DeviceId(deviceId)); device.setTenantId(TenantId.fromUUID(tenantId)); device.setCustomerId(new CustomerId(customerID)); - device.setName(PREFIX_FOR_DEVICE_NAME + number); + device.setName(PREFIX_FOR_DEVICE_NAME + nameSuffix); device.setDeviceProfileId(savedDeviceProfile.getId()); return device; } 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 b9e5e7fc5a..8d289c6e1a 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 @@ -15,15 +15,20 @@ */ package org.thingsboard.server.dao.sql.widget; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; 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.WidgetsBundleWidget; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.widget.WidgetTypeDao; +import org.thingsboard.server.dao.widget.WidgetsBundleDao; import java.util.ArrayList; import java.util.List; @@ -39,15 +44,29 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { final String BUNDLE_ALIAS = "BUNDLE_ALIAS"; final int WIDGET_TYPE_COUNT = 3; List widgetTypeList; + WidgetsBundle widgetsBundle; @Autowired private WidgetTypeDao widgetTypeDao; + @Autowired + private WidgetsBundleDao widgetsBundleDao; + @Before public void setUp() { widgetTypeList = new ArrayList<>(); + + WidgetsBundle widgetsBundle = new WidgetsBundle(); + widgetsBundle.setAlias(BUNDLE_ALIAS); + widgetsBundle.setTitle(BUNDLE_ALIAS); + widgetsBundle.setId(new WidgetsBundleId(Uuids.timeBased())); + widgetsBundle.setTenantId(TenantId.SYS_TENANT_ID); + this.widgetsBundle = widgetsBundleDao.save(TenantId.SYS_TENANT_ID, widgetsBundle); + for (int i = 0; i < WIDGET_TYPE_COUNT; i++) { - widgetTypeList.add(createAndSaveWidgetType(i)); + var widgetType = createAndSaveWidgetType(i); + widgetTypeList.add(widgetType); + widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(this.widgetsBundle.getId(), widgetType.getId(), i)); } } @@ -55,29 +74,29 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(TenantId.SYS_TENANT_ID); widgetType.setName("WIDGET_TYPE_" + number); - widgetType.setAlias("ALIAS_" + number); - widgetType.setBundleAlias(BUNDLE_ALIAS); + widgetType.setFqn("FQN_" + number); return widgetTypeDao.save(TenantId.SYS_TENANT_ID, widgetType); } @After - public void deleteAllWidgetType() { + public void tearDown() { + widgetsBundleDao.removeById(TenantId.SYS_TENANT_ID, widgetsBundle.getUuidId()); for (WidgetType widgetType : widgetTypeList) { widgetTypeDao.removeById(TenantId.SYS_TENANT_ID, widgetType.getUuidId()); } } @Test - public void testFindByTenantIdAndBundleAlias() { - List widgetTypes = widgetTypeDao.findWidgetTypesByTenantIdAndBundleAlias(TenantId.SYS_TENANT_ID.getId(), BUNDLE_ALIAS); + public void testFindByWidgetsBundleId() { + List widgetTypes = widgetTypeDao.findWidgetTypesByWidgetsBundleId(TenantId.SYS_TENANT_ID.getId(), widgetsBundle.getUuidId()); assertEquals(WIDGET_TYPE_COUNT, widgetTypes.size()); } @Test - public void testFindByTenantIdAndBundleAliasAndAlias() { + public void testFindByTenantIdAndFqn() { WidgetType result = widgetTypeList.get(0); assertNotNull(result); - WidgetType widgetType = widgetTypeDao.findByTenantIdBundleAliasAndAlias(TenantId.SYS_TENANT_ID.getId(), BUNDLE_ALIAS, "ALIAS_0"); + WidgetType widgetType = widgetTypeDao.findByTenantIdAndFqn(TenantId.SYS_TENANT_ID.getId(), "FQN_0"); assertEquals(result.getId(), widgetType.getId()); } } 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 209d4193d3..e1758c4660 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 @@ -86,9 +86,9 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { UUID tenantId2 = Uuids.timeBased(); // Create a bunch of widgetBundles for (int i = 0; i < 10; i++) { - createWidgetBundles(3, tenantId1, "WB1_"); - createWidgetBundles(5, tenantId2, "WB2_"); - createSystemWidgetBundles(10, "WB_SYS_"); + createWidgetBundles(3, tenantId1, "WB1_" + i + "_"); + createWidgetBundles(5, tenantId2, "WB2_" + i + "_"); + createSystemWidgetBundles(10, "WB_SYS_" + i + "_"); } widgetsBundles = widgetsBundleDao.find(TenantId.SYS_TENANT_ID); assertEquals(180, widgetsBundleDao.find(TenantId.SYS_TENANT_ID).size()); @@ -112,9 +112,9 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { UUID tenantId2 = Uuids.timeBased(); // Create a bunch of widgetBundles for (int i = 0; i < 10; i++) { - createWidgetBundles(5, tenantId1, "WB1_"); - createWidgetBundles(3, tenantId2, "WB2_"); - createSystemWidgetBundles(2, "WB_SYS_"); + createWidgetBundles(5, tenantId1, "WB1_" + i + "_"); + createWidgetBundles(3, tenantId2, "WB2_" + i + "_"); + createSystemWidgetBundles(2, "WB_SYS_" + i + "_"); } widgetsBundles = widgetsBundleDao.find(TenantId.SYS_TENANT_ID); assertEquals(100, widgetsBundleDao.find(TenantId.SYS_TENANT_ID).size()); diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 98f9091318..8cadda8083 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -77,6 +77,9 @@ cache.specs.entityCount.maxSize=10000 cache.specs.resourceInfo.timeToLiveInMinutes=1440 cache.specs.resourceInfo.maxSize=10000 +cache.specs.alarmTypes.timeToLiveInMinutes=60 +cache.specs.alarmTypes.maxSize=10000 + redis.connection.host=localhost redis.connection.port=6379 redis.connection.db=0 diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 2fca79e01e..9c772df45b 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -19,6 +19,7 @@ DROP TABLE IF EXISTS admin_settings; DROP TABLE IF EXISTS entity_alarm; DROP TABLE IF EXISTS alarm_comment; DROP TABLE IF EXISTS alarm; +DROP TABLE IF EXISTS alarm_type; DROP TABLE IF EXISTS asset; DROP TABLE IF EXISTS audit_log; DROP TABLE IF EXISTS attribute_kv; @@ -37,6 +38,7 @@ DROP TABLE IF EXISTS ts_kv; DROP TABLE IF EXISTS ts_kv_latest; DROP TABLE IF EXISTS ts_kv_dictionary; DROP TABLE IF EXISTS user_credentials; +DROP TABLE IF EXISTS widgets_bundle_widget; DROP TABLE IF EXISTS widget_type; DROP TABLE IF EXISTS widgets_bundle; DROP TABLE IF EXISTS entity_view; diff --git a/monitoring/pom.xml b/monitoring/pom.xml index cc3a78c04d..399e34092d 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 6413973d7c..12d36272c0 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java index 893c8b565c..5a7e9b631d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.msa.connectivity; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; @@ -28,6 +29,7 @@ import lombok.extern.slf4j.Slf4j; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import org.thingsboard.common.util.AbstractListeningExecutor; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; @@ -74,8 +76,18 @@ import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevi public class MqttClientTest extends AbstractContainerTest { private Device device; + AbstractListeningExecutor handlerExecutor; + @BeforeMethod public void setUp() throws Exception { + this.handlerExecutor = new AbstractListeningExecutor() { + @Override + protected int getThreadPollSize() { + return 4; + } + }; + handlerExecutor.init(); + testRestClient.login("tenant@thingsboard.org", "tenant"); device = testRestClient.postDevice("", defaultDevicePrototype("http_")); } @@ -83,6 +95,9 @@ public class MqttClientTest extends AbstractContainerTest { @AfterMethod public void tearDown() { testRestClient.deleteDeviceIfExists(device.getId()); + if (handlerExecutor != null) { + handlerExecutor.destroy(); + } } @Test public void telemetryUpload() throws Exception { @@ -461,11 +476,16 @@ public class MqttClientTest extends AbstractContainerTest { return getMqttClient(deviceCredentials.getCredentialsId(), listener); } + private String getOwnerId() { + return "Tenant[" + device.getTenantId().getId() + "]MqttClientTestDevice[" + device.getId().getId() + "]"; + } + private MqttClient getMqttClient(String username, MqttMessageListener listener) throws InterruptedException, ExecutionException { MqttClientConfig clientConfig = new MqttClientConfig(); + clientConfig.setOwnerId(getOwnerId()); clientConfig.setClientId("MQTT client from test"); clientConfig.setUsername(username); - MqttClient mqttClient = MqttClient.create(clientConfig, listener); + MqttClient mqttClient = MqttClient.create(clientConfig, listener, handlerExecutor); mqttClient.connect("localhost", 1883).get(); return mqttClient; } @@ -479,9 +499,10 @@ public class MqttClientTest extends AbstractContainerTest { } @Override - public void onMessage(String topic, ByteBuf message) { + public ListenableFuture onMessage(String topic, ByteBuf message) { log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic); events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8))); + return Futures.immediateVoidFuture(); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index a038d4cf50..5e2a9f5fe6 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.msa.connectivity; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; @@ -32,6 +33,7 @@ import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import org.thingsboard.common.util.AbstractListeningExecutor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.mqtt.MqttClient; @@ -76,8 +78,18 @@ public class MqttGatewayClientTest extends AbstractContainerTest { private MqttMessageListener listener; private JsonParser jsonParser = new JsonParser(); + AbstractListeningExecutor handlerExecutor; + @BeforeMethod public void createGateway() throws Exception { + this.handlerExecutor = new AbstractListeningExecutor() { + @Override + protected int getThreadPollSize() { + return 4; + } + }; + handlerExecutor.init(); + testRestClient.login("tenant@thingsboard.org", "tenant"); gatewayDevice = testRestClient.postDevice("", defaultGatewayPrototype()); DeviceCredentials gatewayDeviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(gatewayDevice.getId()); @@ -94,6 +106,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest { this.listener = null; this.mqttClient = null; this.createdDevice = null; + if (handlerExecutor != null) { + handlerExecutor.destroy(); + } } @Test @@ -403,11 +418,16 @@ public class MqttGatewayClientTest extends AbstractContainerTest { return testRestClient.getDeviceById(createdDeviceId); } + private String getOwnerId() { + return "Tenant[" + gatewayDevice.getTenantId().getId() + "]MqttGatewayClientTestDevice[" + gatewayDevice.getId().getId() + "]"; + } + private MqttClient getMqttClient(DeviceCredentials deviceCredentials, MqttMessageListener listener) throws InterruptedException, ExecutionException { MqttClientConfig clientConfig = new MqttClientConfig(); + clientConfig.setOwnerId(getOwnerId()); clientConfig.setClientId("MQTT client from test"); clientConfig.setUsername(deviceCredentials.getCredentialsId()); - MqttClient mqttClient = MqttClient.create(clientConfig, listener); + MqttClient mqttClient = MqttClient.create(clientConfig, listener, handlerExecutor); mqttClient.connect("localhost", 1883).get(); return mqttClient; } @@ -421,9 +441,10 @@ public class MqttGatewayClientTest extends AbstractContainerTest { } @Override - public void onMessage(String topic, ByteBuf message) { + public ListenableFuture onMessage(String topic, ByteBuf message) { log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic); events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8))); + return Futures.immediateVoidFuture(); } } diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 7736db173f..dc72a2bb22 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "3.5.2", + "version": "3.6.0", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index fe92a77763..0bd7bf4d60 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index bb0539ac98..ba96ada39c 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT msa diff --git a/msa/pom.xml b/msa/pom.xml index 451466f98a..bf4e15d9d4 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 3d39ba285b..8437cc35c3 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 94382be495..4fd630aafd 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT msa org.thingsboard.msa @@ -38,7 +38,7 @@ tb-postgres tb-cassandra /usr/share/${pkg.name} - 3.5.2 + 3.6.0 diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 7c55ef4322..386b91ea76 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 2d02b2f124..344219a660 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index 7a521a6718..8dfef617cd 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 4ab24589dd..e1d740c877 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index f12564c24f..e0bd5a9457 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index c5b5617f69..b6a4e15b04 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index 162ae1f801..9b6fc493cd 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index c78b3fd129..9bd2befa90 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/src/main/java/org/thingsboard/server/vc/service/VersionControlTenantRoutingInfoService.java b/msa/vc-executor/src/main/java/org/thingsboard/server/vc/service/VersionControlTenantRoutingInfoService.java index fb33ff9931..b343a4791f 100644 --- a/msa/vc-executor/src/main/java/org/thingsboard/server/vc/service/VersionControlTenantRoutingInfoService.java +++ b/msa/vc-executor/src/main/java/org/thingsboard/server/vc/service/VersionControlTenantRoutingInfoService.java @@ -25,6 +25,6 @@ public class VersionControlTenantRoutingInfoService implements TenantRoutingInfo @Override public TenantRoutingInfo getRoutingInfo(TenantId tenantId) { //This dummy implementation is ok since Version Control service does not produce any rule engine messages. - return new TenantRoutingInfo(tenantId, false); + return new TenantRoutingInfo(tenantId, null, false); } } diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index 094e0e2099..ea18768cf3 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -41,6 +41,7 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" queue: type: "${TB_QUEUE_TYPE:kafka}" # in-memory or kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) @@ -72,6 +73,7 @@ queue: fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + auto_offset_reset: "${TB_QUEUE_KAFKA_AUTO_OFFSET_RESET:earliest}" # earliest, latest or none use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" @@ -166,7 +168,7 @@ queue: topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" partitions: "${TB_QUEUE_VC_PARTITIONS:10}" poll-interval: "${TB_QUEUE_VC_INTERVAL_MS:25}" - pack-processing-timeout: "${TB_QUEUE_VC_PACK_PROCESSING_TIMEOUT_MS:60000}" + pack-processing-timeout: "${TB_QUEUE_VC_PACK_PROCESSING_TIMEOUT_MS:180000}" msg-chunk-size: "${TB_QUEUE_VC_MSG_CHUNK_SIZE:250000}" vc: diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index edd232ba57..3338d2ae76 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "3.5.2", + "version": "3.6.0", "description": "ThingsBoard Web UI Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 58efe59856..e9500cf5e0 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index d81be2fece..962a9a8bbc 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT thingsboard netty-mqtt - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT jar Netty MQTT Client @@ -35,6 +35,10 @@ + + org.thingsboard.common + util + io.netty netty-codec-mqtt diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java index fa88fa0254..59744ed194 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java @@ -16,6 +16,10 @@ package org.thingsboard.mqtt; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; @@ -34,9 +38,13 @@ import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttSubAckMessage; import io.netty.handler.codec.mqtt.MqttUnsubAckMessage; import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Promise; import lombok.extern.slf4j.Slf4j; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + @Slf4j final class MqttChannelHandler extends SimpleChannelInboundHandler { @@ -117,27 +125,48 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler super.channelInactive(ctx); } - private void invokeHandlersForIncomingPublish(MqttPublishMessage message) { - boolean handlerInvoked = false; - for (MqttSubscription subscription : ImmutableSet.copyOf(this.client.getSubscriptions().values())) { - if (subscription.matches(message.variableHeader().topicName())) { - if (subscription.isOnce() && subscription.isCalled()) { - continue; - } - message.payload().markReaderIndex(); - subscription.setCalled(true); - subscription.getHandler().onMessage(message.variableHeader().topicName(), message.payload()); - if (subscription.isOnce()) { - this.client.off(subscription.getTopic(), subscription.getHandler()); + ListenableFuture invokeHandlersForIncomingPublish(MqttPublishMessage message) { + var future = Futures.immediateVoidFuture(); + var handlerInvoked = new AtomicBoolean(); + try { + for (MqttSubscription subscription : ImmutableSet.copyOf(this.client.getSubscriptions().values())) { + if (subscription.matches(message.variableHeader().topicName())) { + future = Futures.transform(future, x -> { + if (subscription.isOnce() && subscription.isCalled()) { + return null; + } + message.payload().markReaderIndex(); + subscription.setCalled(true); + subscription.getHandler().onMessage(message.variableHeader().topicName(), message.payload()); + if (subscription.isOnce()) { + this.client.off(subscription.getTopic(), subscription.getHandler()); + } + message.payload().resetReaderIndex(); + handlerInvoked.set(true); + return null; + }, client.getHandlerExecutor()); } - message.payload().resetReaderIndex(); - handlerInvoked = true; } + future = Futures.transform(future, x -> { + if (!handlerInvoked.get() && client.getDefaultHandler() != null) { + client.getDefaultHandler().onMessage(message.variableHeader().topicName(), message.payload()); + } + return null; + }, client.getHandlerExecutor()); + } finally { + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + message.payload().release(); + } + + @Override + public void onFailure(Throwable t) { + message.payload().release(); + } + }, MoreExecutors.directExecutor()); } - if (!handlerInvoked && client.getDefaultHandler() != null) { - client.getDefaultHandler().onMessage(message.variableHeader().topicName(), message.payload()); - } - message.payload().release(); + return future; } private void handleConack(Channel channel, MqttConnAckMessage message) { @@ -204,11 +233,13 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler break; case AT_LEAST_ONCE: - invokeHandlersForIncomingPublish(message); + var future = invokeHandlersForIncomingPublish(message); if (message.variableHeader().packetId() != -1) { - MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0); - MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().packetId()); - channel.writeAndFlush(new MqttPubAckMessage(fixedHeader, variableHeader)); + future.addListener(() -> { + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0); + MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().packetId()); + channel.writeAndFlush(new MqttPubAckMessage(fixedHeader, variableHeader)); + }, MoreExecutors.directExecutor()); } break; @@ -263,14 +294,20 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler } private void handlePubrel(Channel channel, MqttMessage message) { + var future = Futures.immediateVoidFuture(); if (this.client.getQos2PendingIncomingPublishes().containsKey(((MqttMessageIdVariableHeader) message.variableHeader()).messageId())) { MqttIncomingQos2Publish incomingQos2Publish = this.client.getQos2PendingIncomingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId()); - this.invokeHandlersForIncomingPublish(incomingQos2Publish.getIncomingPublish()); - this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().packetId()); + future = invokeHandlersForIncomingPublish(incomingQos2Publish.getIncomingPublish()); + future = Futures.transform(future, x -> { + this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().packetId()); + return null; + }, MoreExecutors.directExecutor()); } - MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0); - MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(((MqttMessageIdVariableHeader) message.variableHeader()).messageId()); - channel.writeAndFlush(new MqttMessage(fixedHeader, variableHeader)); + future.addListener(() -> { + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0); + MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(((MqttMessageIdVariableHeader) message.variableHeader()).messageId()); + channel.writeAndFlush(new MqttMessage(fixedHeader, variableHeader)); + }, MoreExecutors.directExecutor()); } private void handlePubcomp(MqttMessage message) { @@ -281,4 +318,21 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler pendingPublish.getPayload().release(); pendingPublish.onPubcompReceived(); } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + try { + if (cause instanceof IOException) { + if (log.isDebugEnabled()) { + log.debug("[{}] IOException: ", client.getClientConfig().getOwnerId(), cause); + } else { + log.info("[{}] IOException: {}", client.getClientConfig().getOwnerId(), cause.getMessage()); + } + } else { + log.warn("[{}] exceptionCaught", client.getClientConfig().getOwnerId(), cause); + } + } finally { + ReferenceCountUtil.release(cause); + } + } } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java index 2fe179de31..536a76119f 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java @@ -21,6 +21,7 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.util.concurrent.Future; +import org.thingsboard.common.util.ListeningExecutor; public interface MqttClient { @@ -71,6 +72,8 @@ public interface MqttClient { */ void setEventLoop(EventLoopGroup eventLoop); + ListeningExecutor getHandlerExecutor(); + /** * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler * @@ -180,8 +183,8 @@ public interface MqttClient { * @param config The config object to use while looking for settings * @param defaultHandler The handler for incoming messages that do not match any topic subscriptions */ - static MqttClient create(MqttClientConfig config, MqttHandler defaultHandler){ - return new MqttClientImpl(config, defaultHandler); + static MqttClient create(MqttClientConfig config, MqttHandler defaultHandler, ListeningExecutor handlerExecutor){ + return new MqttClientImpl(config, defaultHandler, handlerExecutor); } /** diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java index 1e20a842c3..10cee5d5fc 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java @@ -19,6 +19,8 @@ import io.netty.channel.Channel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.mqtt.MqttVersion; import io.netty.handler.ssl.SslContext; +import lombok.Getter; +import lombok.Setter; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -26,10 +28,12 @@ import java.util.Random; @SuppressWarnings({"WeakerAccess", "unused"}) public final class MqttClientConfig { - private final SslContext sslContext; private final String randomClientId; + @Getter + @Setter + private String ownerId; // [TenantId][IntegrationId] or [TenantId][RuleNodeId] for exceptions logging purposes private String clientId; private int timeoutSeconds = 60; private MqttVersion protocolVersion = MqttVersion.MQTT_3_1; diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java index f38a790be7..63d65a1cc2 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -46,6 +46,7 @@ import io.netty.util.concurrent.DefaultPromise; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.ListeningExecutor; import java.util.Collections; import java.util.HashSet; @@ -88,13 +89,13 @@ final class MqttClientImpl implements MqttClient { private int port; private MqttClientCallback callback; + private final ListeningExecutor handlerExecutor; /** * Construct the MqttClientImpl with default config */ - public MqttClientImpl(MqttHandler defaultHandler) { - this.clientConfig = new MqttClientConfig(); - this.defaultHandler = defaultHandler; + public MqttClientImpl(MqttHandler defaultHandler, ListeningExecutor handlerExecutor) { + this(new MqttClientConfig(), defaultHandler, handlerExecutor); } /** @@ -103,9 +104,10 @@ final class MqttClientImpl implements MqttClient { * * @param clientConfig The config object to use while looking for settings */ - public MqttClientImpl(MqttClientConfig clientConfig, MqttHandler defaultHandler) { + public MqttClientImpl(MqttClientConfig clientConfig, MqttHandler defaultHandler, ListeningExecutor handlerExecutor) { this.clientConfig = clientConfig; this.defaultHandler = defaultHandler; + this.handlerExecutor = handlerExecutor; } /** @@ -227,6 +229,11 @@ final class MqttClientImpl implements MqttClient { this.eventLoop = eventLoop; } + @Override + public ListeningExecutor getHandlerExecutor() { + return this.handlerExecutor; + } + /** * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler * diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java index 0ec03ff04b..21c07a17cd 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java @@ -15,9 +15,10 @@ */ package org.thingsboard.mqtt; +import com.google.common.util.concurrent.ListenableFuture; import io.netty.buffer.ByteBuf; public interface MqttHandler { - void onMessage(String topic, ByteBuf payload); + ListenableFuture onMessage(String topic, ByteBuf payload); } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java index 6c4abb4c5c..c4bc9e38c1 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java @@ -25,7 +25,7 @@ final class MqttSubscription { private final boolean once; - private boolean called; + private volatile boolean called; MqttSubscription(String topic, MqttHandler handler, boolean once) { if (topic == null) { diff --git a/netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/MqttIntegrationTest.java b/netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/MqttIntegrationTest.java index cb1b6b81fe..04c2be1740 100644 --- a/netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/MqttIntegrationTest.java +++ b/netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/MqttIntegrationTest.java @@ -26,6 +26,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.thingsboard.common.util.AbstractListeningExecutor; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttConnectResult; @@ -49,8 +50,18 @@ public class MqttIntegrationTest { MqttClient mqttClient; + AbstractListeningExecutor handlerExecutor; + @Before public void init() throws Exception { + this.handlerExecutor = new AbstractListeningExecutor() { + @Override + protected int getThreadPollSize() { + return 4; + } + }; + handlerExecutor.init(); + this.eventLoopGroup = new NioEventLoopGroup(); this.mqttServer = new MqttServer(); @@ -68,6 +79,9 @@ public class MqttIntegrationTest { if (this.eventLoopGroup != null) { this.eventLoopGroup.shutdownGracefully(0, 0, TimeUnit.MILLISECONDS); } + if (this.handlerExecutor != null) { + this.handlerExecutor.destroy(); + } } @Test @@ -108,9 +122,10 @@ public class MqttIntegrationTest { private MqttClient initClient() throws Exception { MqttClientConfig config = new MqttClientConfig(); + config.setOwnerId("MqttIntegrationTest"); config.setTimeoutSeconds(KEEPALIVE_TIMEOUT_SECONDS); config.setReconnectDelay(RECONNECT_DELAY_SECONDS); - MqttClient client = MqttClient.create(config, null); + MqttClient client = MqttClient.create(config, null, handlerExecutor); client.setEventLoop(this.eventLoopGroup); Future connectFuture = client.connect(MQTT_HOST, this.mqttServer.getMqttPort()); diff --git a/pom.xml b/pom.xml index 59d1eff5d3..69b9850ef1 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT pom Thingsboard @@ -78,7 +78,7 @@ 3.8.1 3.21.9 1.42.1 - 1.0.6 + 1.0.9 1.18.18 1.2.4 1.2.5 diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 728ffc5040..638dc42009 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT thingsboard rest-client 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 51eb446d70..fb878a33b6 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 @@ -499,6 +499,15 @@ public class RestClient implements Closeable { return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody(); } + public List getAlarmTypes(PageLink pageLink) { + return restTemplate.exchange( + baseURL + "/api/alarm/types?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + public AlarmComment saveAlarmComment(AlarmId alarmId, AlarmComment alarmComment) { return restTemplate.postForEntity(baseURL + "/api/alarm/{alarmId}/comment", alarmComment, AlarmComment.class, alarmId.getId()).getBody(); } @@ -2364,7 +2373,8 @@ public class RestClient implements Closeable { boolean deleteAllDataForKeys, Long startTs, Long endTs, - boolean rewriteLatestIfDeleted) { + boolean rewriteLatestIfDeleted, + boolean deleteLatest) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); params.put("entityId", entityId.getId().toString()); @@ -2373,17 +2383,34 @@ public class RestClient implements Closeable { params.put("startTs", startTs.toString()); params.put("endTs", endTs.toString()); params.put("rewriteLatestIfDeleted", String.valueOf(rewriteLatestIfDeleted)); + params.put("deleteLatest", String.valueOf(deleteLatest)); return restTemplate .exchange( - baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/timeseries/delete?keys={keys}&deleteAllDataForKeys={deleteAllDataForKeys}&startTs={startTs}&endTs={endTs}&rewriteLatestIfDeleted={rewriteLatestIfDeleted}", + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/timeseries/delete?keys={keys}&deleteAllDataForKeys={deleteAllDataForKeys}&startTs={startTs}&endTs={endTs}&rewriteLatestIfDeleted={rewriteLatestIfDeleted}&deleteLatest={deleteLatest}", HttpMethod.DELETE, HttpEntity.EMPTY, Object.class, params) .getStatusCode() .is2xxSuccessful(); + } + public boolean deleteEntityLatestTimeseries(EntityId entityId, List keys) { + Map params = new HashMap<>(); + params.put("entityType", entityId.getEntityType().name()); + params.put("entityId", entityId.getId().toString()); + params.put("keys", listToString(keys)); + + return restTemplate + .exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/timeseries/latest/delete?keys={keys}", + HttpMethod.DELETE, + HttpEntity.EMPTY, + Object.class, + params) + .getStatusCode() + .is2xxSuccessful(); } public boolean deleteEntityAttributes(DeviceId deviceId, String scope, List keys) { diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 3ebb1f6c76..fe8cb3fab5 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index f8c45d388b..47d13e5658 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java index 5c54687205..22ffe34769 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java @@ -24,7 +24,6 @@ public class EmptyNodeConfiguration implements NodeConfiguration callback); + void sendGeneralWebNotification(TenantId tenantId, UsersFilter recipients, NotificationTemplate template); + void deleteNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId); void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java index 05e9bf164f..ebaff2bfaf 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.api; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; @@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.alarm.AlarmOperationResult; @@ -54,6 +56,7 @@ public interface RuleEngineAlarmService { * Only one active alarm may exist for the pair {originatorId, alarmType} */ AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request); + /** * Designed to update existing alarm. Accepts only part of the alarm fields. */ @@ -110,4 +113,6 @@ public interface RuleEngineAlarmService { AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId); PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); + + PageData findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java index 7ee61bac1d..7037f2f3fe 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.api; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentClusteringMode; import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -47,7 +48,7 @@ public @interface RuleNode { ComponentScope scope() default ComponentScope.TENANT; - String[] relationTypes() default {"Success", "Failure"}; + String[] relationTypes() default {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE}; String[] uiResources() default {}; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 88bf80e9f9..e645110a9d 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleNode; @@ -183,21 +184,52 @@ public interface TbContext { void ack(TbMsg tbMsg); + @Deprecated(since = "3.6.0", forRemoval = true) TbMsg newMsg(String queueName, String type, EntityId originator, TbMsgMetaData metaData, String data); + /** + * Creates a new TbMsg instance with the specified parameters. + * + *

Deprecated: This method is deprecated since version 3.6.0 and should only be used when you need to + * specify a custom message type that doesn't exist in the {@link TbMsgType} enum. For standard message types, + * it is recommended to use the {@link #newMsg(String, TbMsgType, EntityId, CustomerId, TbMsgMetaData, String)} + * method instead.

+ * + * @param queueName the name of the queue where the message will be sent + * @param type the type of the message + * @param originator the originator of the message + * @param customerId the ID of the customer associated with the message + * @param metaData the metadata of the message + * @param data the data of the message + * @return new TbMsg instance + */ + @Deprecated(since = "3.6.0") TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data); + @Deprecated(since = "3.6.0", forRemoval = true) TbMsg transformMsg(TbMsg origMsg, String type, EntityId originator, TbMsgMetaData metaData, String data); + TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data); + + TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data); + + TbMsg transformMsg(TbMsg origMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data); + + TbMsg transformMsg(TbMsg origMsg, TbMsgMetaData metaData, String data); + + TbMsg transformMsgOriginator(TbMsg origMsg, EntityId originator); + TbMsg customerCreatedMsg(Customer customer, RuleNodeId ruleNodeId); TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId); TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId); - // TODO: Does this changes the message? + @Deprecated(since = "3.6.0", forRemoval = true) TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action); + TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, TbMsgType actionMsgType); + TbMsg attributesUpdatedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List attributes); TbMsg attributesDeletedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List keys); diff --git a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java index 956cfaf40c..cb5514a82b 100644 --- a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java +++ b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java @@ -22,6 +22,7 @@ import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -43,7 +44,7 @@ public class TbNodeUtilsTest { ObjectNode node = JacksonUtil.newObjectNode(); node.put("data_key", "data_value"); - TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); String result = TbNodeUtils.processPattern(pattern, msg); Assert.assertEquals("ABC metadata_value data_value", result); } @@ -57,7 +58,7 @@ public class TbNodeUtilsTest { ObjectNode node = JacksonUtil.newObjectNode(); node.put("key", "data_value"); - TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); String result = TbNodeUtils.processPattern(pattern, msg); Assert.assertEquals(pattern, result); } @@ -71,7 +72,7 @@ public class TbNodeUtilsTest { ObjectNode node = JacksonUtil.newObjectNode(); node.put("key", "data_value"); - TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); String result = TbNodeUtils.processPattern(pattern, msg); Assert.assertEquals("ABC metadata_value data_value", result); } @@ -92,7 +93,7 @@ public class TbNodeUtilsTest { ObjectNode node = JacksonUtil.newObjectNode(); node.set("key1", key1Node); - TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); String result = TbNodeUtils.processPattern(pattern, msg); Assert.assertEquals("ABC metadata_value value3", result); } @@ -113,7 +114,7 @@ public class TbNodeUtilsTest { ObjectNode node = JacksonUtil.newObjectNode(); node.set("key1", key1Node); - TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); String result = TbNodeUtils.processPattern(pattern, msg); Assert.assertEquals("ABC metadata_value $[key1.key2[0].key3]", result); } diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index ac493e79ce..34c6e315f7 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java index 3ca55f5db9..57871c91ff 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java @@ -26,6 +26,8 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -55,13 +57,13 @@ public abstract class TbAbstractAlarmNode { if (alarmResult.alarm == null) { - ctx.tellNext(msg, "False"); + ctx.tellNext(msg, TbNodeConnectionType.FALSE); } else if (alarmResult.isCreated) { - tellNext(ctx, msg, alarmResult, DataConstants.ENTITY_CREATED, "Created"); + tellNext(ctx, msg, alarmResult, TbMsgType.ENTITY_CREATED, "Created"); } else if (alarmResult.isUpdated) { - tellNext(ctx, msg, alarmResult, DataConstants.ENTITY_UPDATED, "Updated"); + tellNext(ctx, msg, alarmResult, TbMsgType.ENTITY_UPDATED, "Updated"); } else if (alarmResult.isCleared) { - tellNext(ctx, msg, alarmResult, DataConstants.ALARM_CLEAR, "Cleared"); + tellNext(ctx, msg, alarmResult, TbMsgType.ALARM_CLEAR, "Cleared"); } else { ctx.tellSuccess(msg); } @@ -71,13 +73,13 @@ public abstract class TbAbstractAlarmNode processAlarm(TbContext ctx, TbMsg msg); - protected ListenableFuture buildAlarmDetails(TbContext ctx, TbMsg msg, JsonNode previousDetails) { + protected ListenableFuture buildAlarmDetails(TbMsg msg, JsonNode previousDetails) { try { TbMsg dummyMsg = msg; if (previousDetails != null) { TbMsgMetaData metaData = msg.getMetaData().copy(); metaData.putValue(PREV_ALARM_DETAILS, JacksonUtil.toString(previousDetails)); - dummyMsg = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), metaData, msg.getData()); + dummyMsg = TbMsg.transformMsgMetadata(msg, metaData); } return scriptEngine.executeJsonAsync(dummyMsg); } catch (Exception e) { @@ -96,7 +98,7 @@ public abstract class TbAbstractAlarmNode ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), alarmAction), throwable -> ctx.tellFailure(toAlarmMsg(ctx, alarmResult, msg), throwable)); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java index 1ed7689bd6..ea53138d10 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java @@ -57,8 +57,8 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; import static org.thingsboard.common.util.DonAsynchron.withCallback; -import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; +import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.FAILURE; +import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.SUCCESS; @Slf4j public abstract class TbAbstractRelationActionNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index a8d7d4985f..d0215a441f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -71,7 +71,7 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { ctx.logJsEvalRequest(); - ListenableFuture asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails()); + ListenableFuture asyncDetails = buildAlarmDetails(msg, alarm.getDetails()); return Futures.transform(asyncDetails, details -> { ctx.logJsEvalResponse(); AlarmApiCallResult result = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), System.currentTimeMillis(), details); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java index 6f7bb7cf8d..c103dd4006 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java @@ -36,7 +36,6 @@ import org.thingsboard.server.common.data.objects.AttributesEntityView; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import javax.annotation.Nullable; @@ -45,7 +44,12 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; +import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.INACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; +import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.SUCCESS; @Slf4j @RuleNode( @@ -71,14 +75,11 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (DataConstants.ATTRIBUTES_UPDATED.equals(msg.getType()) || - DataConstants.ATTRIBUTES_DELETED.equals(msg.getType()) || - DataConstants.ACTIVITY_EVENT.equals(msg.getType()) || - DataConstants.INACTIVITY_EVENT.equals(msg.getType()) || - SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msg.getType())) { + if (msg.isTypeOneOf(ATTRIBUTES_UPDATED, ATTRIBUTES_DELETED, + ACTIVITY_EVENT, INACTIVITY_EVENT, POST_ATTRIBUTES_REQUEST)) { if (!msg.getMetaData().getData().isEmpty()) { long now = System.currentTimeMillis(); - String scope = msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()) ? + String scope = msg.isTypeOf(POST_ATTRIBUTES_REQUEST) ? DataConstants.CLIENT_SCOPE : msg.getMetaData().getValue(DataConstants.SCOPE); ListenableFuture> entityViewsFuture = @@ -90,9 +91,9 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { long startTime = entityView.getStartTimeMs(); long endTime = entityView.getEndTimeMs(); if ((endTime != 0 && endTime > now && startTime < now) || (endTime == 0 && startTime < now)) { - if (DataConstants.ATTRIBUTES_DELETED.equals(msg.getType())) { + if (msg.isTypeOf(ATTRIBUTES_DELETED)) { List attributes = new ArrayList<>(); - for (JsonElement element : new JsonParser().parse(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray()) { + for (JsonElement element : JsonParser.parseString(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray()) { if (element.isJsonPrimitive()) { JsonPrimitive value = element.getAsJsonPrimitive(); if (value.isString()) { @@ -107,7 +108,7 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { getFutureCallback(ctx, msg, entityView)); } } else { - Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())); + Set attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); List filteredAttributes = attributes.stream().filter(attr -> attributeContainsInEntityView(scope, attr.getKey(), entityView)).collect(Collectors.toList()); ctx.getTelemetryService().saveAndNotify(ctx.getTenantId(), entityView.getId(), scope, filteredAttributes, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index 84e82b76e8..b08fd1a562 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -120,7 +120,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode future = createRelationIfAbsent(ctx, msg, entity, relationType); return Futures.transform(future, result -> { if (result && config.isChangeOriginatorToRelatedEntity()) { - TbMsg tbMsg = ctx.transformMsg(msg, msg.getType(), entity.getEntityId(), msg.getMetaData(), msg.getData()); + TbMsg tbMsg = ctx.transformMsgOriginator(msg, entity.getEntityId()); return new RelationContainer(tbMsg, result); } return new RelationContainer(msg, result); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java index 88f2be46a8..021123a308 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java @@ -24,17 +24,16 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; - @Slf4j @RuleNode( type = ComponentType.ACTION, @@ -48,8 +47,6 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; ) public class TbMsgCountNode implements TbNode { - private static final String TB_MSG_COUNT_NODE_MSG = "TbMsgCountNodeMsg"; - private AtomicLong messagesProcessed = new AtomicLong(0); private final Gson gson = new Gson(); private UUID nextTickId; @@ -68,7 +65,7 @@ public class TbMsgCountNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (msg.getType().equals(TB_MSG_COUNT_NODE_MSG) && msg.getId().equals(nextTickId)) { + if (msg.isTypeOf(TbMsgType.MSG_COUNT_SELF_MSG) && msg.getId().equals(nextTickId)) { JsonObject telemetryJson = new JsonObject(); telemetryJson.addProperty(this.telemetryPrefix + "_" + ctx.getServiceId(), messagesProcessed.longValue()); @@ -77,8 +74,8 @@ public class TbMsgCountNode implements TbNode { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("delta", Long.toString(System.currentTimeMillis() - lastScheduledTs + delay)); - TbMsg tbMsg = TbMsg.newMsg(msg.getQueueName(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), ctx.getTenantId(), msg.getCustomerId(), metaData, gson.toJson(telemetryJson)); - ctx.enqueueForTellNext(tbMsg, SUCCESS); + TbMsg tbMsg = TbMsg.newMsg(msg.getQueueName(), TbMsgType.POST_TELEMETRY_REQUEST, ctx.getTenantId(), msg.getCustomerId(), metaData, gson.toJson(telemetryJson)); + ctx.enqueueForTellNext(tbMsg, TbNodeConnectionType.SUCCESS); scheduleTickMsg(ctx, tbMsg); } else { messagesProcessed.incrementAndGet(); @@ -93,7 +90,7 @@ public class TbMsgCountNode implements TbNode { } lastScheduledTs = lastScheduledTs + delay; long curDelay = Math.max(0L, (lastScheduledTs - curTs)); - TbMsg tickMsg = ctx.newMsg(null, TB_MSG_COUNT_NODE_MSG, ctx.getSelfId(), msg != null ? msg.getCustomerId() : null, new TbMsgMetaData(), ""); + TbMsg tickMsg = ctx.newMsg(null, TbMsgType.MSG_COUNT_SELF_MSG, ctx.getSelfId(), msg != null ? msg.getCustomerId() : null, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); nextTickId = tickMsg.getId(); ctx.tellSelf(tickMsg, curDelay); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java index a8a1137a98..e24c73f9d3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java @@ -29,7 +29,6 @@ import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -106,13 +105,13 @@ public class TbSnsNode extends TbAbstractExternalNode { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(MESSAGE_ID, result.getMessageId()); metaData.putValue(REQUEST_ID, result.getSdkResponseMetadata().getRequestId()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java index 2b9f8916cf..34072fbb8a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java @@ -28,7 +28,6 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; @@ -93,14 +92,14 @@ public class TbSqsNode extends TbAbstractExternalNode { var tbMsg = ackIfNeeded(ctx, msg); withCallback(publishMessageAsync(ctx, tbMsg), m -> tellSuccess(ctx, m), - t -> tellFailure(ctx, processException(ctx, tbMsg, t), t)); + t -> tellFailure(ctx, processException(tbMsg, t), t)); } private ListenableFuture publishMessageAsync(TbContext ctx, TbMsg msg) { - return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg)); + return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(msg)); } - private TbMsg publishMessage(TbContext ctx, TbMsg msg) { + private TbMsg publishMessage(TbMsg msg) { String queueUrl = TbNodeUtils.processPattern(this.config.getQueueUrlPattern(), msg); SendMessageRequest sendMsgRequest = new SendMessageRequest(); sendMsgRequest.withQueueUrl(queueUrl); @@ -119,10 +118,10 @@ public class TbSqsNode extends TbAbstractExternalNode { sendMsgRequest.withMessageGroupId(msg.getOriginator().toString()); } SendMessageResult result = this.sqsClient.sendMessage(sendMsgRequest); - return processSendMessageResult(ctx, msg, result); + return processSendMessageResult(msg, result); } - private TbMsg processSendMessageResult(TbContext ctx, TbMsg origMsg, SendMessageResult result) { + private TbMsg processSendMessageResult(TbMsg origMsg, SendMessageResult result) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(MESSAGE_ID, result.getMessageId()); metaData.putValue(REQUEST_ID, result.getSdkResponseMetadata().getRequestId()); @@ -135,13 +134,13 @@ public class TbSqsNode extends TbAbstractExternalNode { if (!StringUtils.isEmpty(result.getSequenceNumber())) { metaData.putValue(SEQUENCE_NUMBER, result.getSequenceNumber()); } - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) { + private TbMsg processException(TbMsg origMsg, Throwable t) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 51effe02ef..b31c98bc0f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -28,8 +28,11 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; @@ -41,7 +44,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import static org.thingsboard.common.util.DonAsynchron.withCallback; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; @Slf4j @RuleNode( @@ -58,8 +60,6 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; public class TbMsgGeneratorNode implements TbNode { - private static final String TB_MSG_GENERATOR_NODE_MSG = "TbMsgGeneratorNodeMsg"; - private TbMsgGeneratorNodeConfiguration config; private ScriptEngine scriptEngine; private long delay; @@ -97,7 +97,7 @@ public class TbMsgGeneratorNode implements TbNode { if (initialized.compareAndSet(false, true)) { this.scriptEngine = ctx.createScriptEngine(config.getScriptLang(), ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType"); - scheduleTickMsg(ctx); + scheduleTickMsg(ctx, null); } } else if (initialized.compareAndSet(true, false)) { destroy(); @@ -107,14 +107,14 @@ public class TbMsgGeneratorNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { log.trace("onMsg, config {}, msg {}", config, msg); - if (initialized.get() && msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) { + if (initialized.get() && msg.isTypeOf(TbMsgType.GENERATOR_NODE_SELF_MSG) && msg.getId().equals(nextTickId)) { TbStopWatch sw = TbStopWatch.create(); withCallback(generate(ctx, msg), m -> { log.trace("onMsg onSuccess callback, took {}ms, config {}, msg {}", sw.stopAndGetTotalTimeMillis(), config, msg); if (initialized.get() && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) { - ctx.enqueueForTellNext(m, SUCCESS); - scheduleTickMsg(ctx); + ctx.enqueueForTellNext(m, TbNodeConnectionType.SUCCESS); + scheduleTickMsg(ctx, msg); currentMsgCount++; } }, @@ -122,14 +122,14 @@ public class TbMsgGeneratorNode implements TbNode { log.trace("onMsg onFailure callback, took {}ms, config {}, msg {}", sw.stopAndGetTotalTimeMillis(), config, msg, t); if (initialized.get() && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) { ctx.tellFailure(msg, t); - scheduleTickMsg(ctx); + scheduleTickMsg(ctx, msg); currentMsgCount++; } }); } } - private void scheduleTickMsg(TbContext ctx) { + private void scheduleTickMsg(TbContext ctx, TbMsg msg) { log.trace("scheduleTickMsg, config {}", config); long curTs = System.currentTimeMillis(); if (lastScheduledTs == 0L) { @@ -137,7 +137,8 @@ public class TbMsgGeneratorNode implements TbNode { } lastScheduledTs = lastScheduledTs + delay; long curDelay = Math.max(0L, (lastScheduledTs - curTs)); - TbMsg tickMsg = ctx.newMsg(config.getQueueName(), TB_MSG_GENERATOR_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), ""); + TbMsg tickMsg = ctx.newMsg(config.getQueueName(), TbMsgType.GENERATOR_NODE_SELF_MSG, ctx.getSelfId(), + getCustomerIdFromMsg(msg), TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); nextTickId = tickMsg.getId(); ctx.tellSelf(tickMsg, curDelay); } @@ -145,7 +146,7 @@ public class TbMsgGeneratorNode implements TbNode { private ListenableFuture generate(TbContext ctx, TbMsg msg) { log.trace("generate, config {}", config); if (prevMsg == null) { - prevMsg = ctx.newMsg(config.getQueueName(), "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}"); + prevMsg = ctx.newMsg(config.getQueueName(), TbMsg.EMPTY_STRING, originatorId, msg.getCustomerId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); } if (initialized.get()) { ctx.logJsEvalRequest(); @@ -160,6 +161,10 @@ public class TbMsgGeneratorNode implements TbNode { } + private CustomerId getCustomerIdFromMsg(TbMsg msg) { + return msg != null ? msg.getCustomerId() : null; + } + @Override public void destroy() { log.trace("destroy, config {}", config); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java index 6890aa6bf4..81bb3d6772 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java @@ -24,9 +24,10 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; @@ -60,10 +61,7 @@ import java.util.concurrent.TimeUnit; @Slf4j public class TbMsgDeduplicationNode implements TbNode { - private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg"; public static final int TB_MSG_DEDUPLICATION_RETRY_DELAY = 10; - private static final String EMPTY_DATA = ""; - private static final TbMsgMetaData EMPTY_META_DATA = new TbMsgMetaData(); private TbMsgDeduplicationNodeConfiguration config; @@ -82,7 +80,7 @@ public class TbMsgDeduplicationNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { - if (TB_MSG_DEDUPLICATION_TIMEOUT_MSG.equals(msg.getType())) { + if (msg.isTypeOf(TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG)) { processDeduplication(ctx, msg.getOriginator()); } else { processOnRegularMsg(ctx, msg); @@ -196,7 +194,7 @@ public class TbMsgDeduplicationNode implements TbNode { private void enqueueForTellNextWithRetry(TbContext ctx, TbMsg msg, int retryAttempt) { if (config.getMaxRetries() > retryAttempt) { - ctx.enqueueForTellNext(msg, TbRelationTypes.SUCCESS, + ctx.enqueueForTellNext(msg, TbNodeConnectionType.SUCCESS, () -> { log.trace("[{}][{}][{}] Successfully enqueue deduplication result message!", ctx.getSelfId(), msg.getOriginator(), retryAttempt); }, @@ -210,7 +208,7 @@ public class TbMsgDeduplicationNode implements TbNode { } private void scheduleTickMsg(TbContext ctx, EntityId deduplicationId) { - ctx.tellSelf(ctx.newMsg(null, TB_MSG_DEDUPLICATION_TIMEOUT_MSG, deduplicationId, EMPTY_META_DATA, EMPTY_DATA), deduplicationInterval + 1); + ctx.tellSelf(ctx.newMsg(null, TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG, deduplicationId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING), deduplicationInterval + 1); } private String getMergedData(List msgs) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java index 1f18aea86e..5414cc2bbe 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java @@ -23,6 +23,8 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -32,8 +34,6 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; - @Slf4j @RuleNode( type = ComponentType.ACTION, @@ -50,8 +50,6 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; ) public class TbMsgDelayNode implements TbNode { - private static final String TB_MSG_DELAY_NODE_MSG = "TbMsgDelayNodeMsg"; - private TbMsgDelayNodeConfiguration config; private Map pendingMsgs; @@ -63,7 +61,7 @@ public class TbMsgDelayNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (msg.getType().equals(TB_MSG_DELAY_NODE_MSG)) { + if (msg.isTypeOf(TbMsgType.DELAY_TIMEOUT_SELF_MSG)) { TbMsg pendingMsg = pendingMsgs.remove(UUID.fromString(msg.getData())); if (pendingMsg != null) { ctx.enqueueForTellNext( @@ -75,13 +73,13 @@ public class TbMsgDelayNode implements TbNode { pendingMsg.getMetaData(), pendingMsg.getData() ), - SUCCESS + TbNodeConnectionType.SUCCESS ); } } else { if (pendingMsgs.size() < config.getMaxPendingMsgs()) { pendingMsgs.put(msg.getId(), msg); - TbMsg tickMsg = ctx.newMsg(null, TB_MSG_DELAY_NODE_MSG, ctx.getSelfId(), msg.getCustomerId(), new TbMsgMetaData(), msg.getId().toString()); + TbMsg tickMsg = ctx.newMsg(null, TbMsgType.DELAY_TIMEOUT_SELF_MSG, ctx.getSelfId(), msg.getCustomerId(), TbMsgMetaData.EMPTY, msg.getId().toString()); ctx.tellSelf(tickMsg, getDelay(msg)); ctx.ack(msg); } else { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java index 318b38aa39..7888922dc6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java @@ -30,13 +30,23 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.session.SessionMsgType; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.CONNECT_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.DISCONNECT_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.INACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; +import static org.thingsboard.server.common.data.msg.TbMsgType.TIMESERIES_UPDATED; + @Slf4j public abstract class AbstractTbMsgPushNode implements TbNode { @@ -57,7 +67,7 @@ public abstract class AbstractTbMsgPushNode metadata = msg.getMetaData().getData(); - EdgeEventActionType actionType = getEdgeEventActionTypeByMsgType(msgType, metadata); + EdgeEventActionType actionType = getEdgeEventActionTypeByMsgType(msg); Map entityBody = new HashMap<>(); JsonNode dataJson = JacksonUtil.toJsonNode(msg.getData()); switch (actionType) { @@ -148,45 +157,31 @@ public abstract class AbstractTbMsgPushNode metadata) { + protected EdgeEventActionType getEdgeEventActionTypeByMsgType(TbMsg msg) { EdgeEventActionType actionType; - if (SessionMsgType.POST_TELEMETRY_REQUEST.name().equals(msgType) - || DataConstants.TIMESERIES_UPDATED.equals(msgType)) { + if (msg.isTypeOneOf(POST_TELEMETRY_REQUEST, TIMESERIES_UPDATED)) { actionType = EdgeEventActionType.TIMESERIES_UPDATED; - } else if (DataConstants.ATTRIBUTES_UPDATED.equals(msgType)) { + } else if (msg.isTypeOf(ATTRIBUTES_UPDATED)) { actionType = EdgeEventActionType.ATTRIBUTES_UPDATED; - } else if (SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msgType)) { + } else if (msg.isTypeOf(POST_ATTRIBUTES_REQUEST)) { actionType = EdgeEventActionType.POST_ATTRIBUTES; - } else if (DataConstants.ATTRIBUTES_DELETED.equals(msgType)) { + } else if (msg.isTypeOf(ATTRIBUTES_DELETED)) { actionType = EdgeEventActionType.ATTRIBUTES_DELETED; - } else if (DataConstants.CONNECT_EVENT.equals(msgType) - || DataConstants.DISCONNECT_EVENT.equals(msgType) - || DataConstants.ACTIVITY_EVENT.equals(msgType) - || DataConstants.INACTIVITY_EVENT.equals(msgType)) { - String scope = metadata.get(SCOPE); - if ( StringUtils.isEmpty(scope)) { - actionType = EdgeEventActionType.TIMESERIES_UPDATED; - } else { - actionType = EdgeEventActionType.ATTRIBUTES_UPDATED; - } + } else if (msg.isTypeOneOf(CONNECT_EVENT, DISCONNECT_EVENT, ACTIVITY_EVENT, INACTIVITY_EVENT)) { + String scope = msg.getMetaData().getValue(SCOPE); + actionType = StringUtils.isEmpty(scope) ? + EdgeEventActionType.TIMESERIES_UPDATED : EdgeEventActionType.ATTRIBUTES_UPDATED; } else { - log.warn("Unsupported msg type [{}]", msgType); - throw new IllegalArgumentException("Unsupported msg type: " + msgType); + String type = msg.getType(); + log.warn("Unsupported msg type [{}]", type); + throw new IllegalArgumentException("Unsupported msg type: " + type); } return actionType; } - protected boolean isSupportedMsgType(String msgType) { - return SessionMsgType.POST_TELEMETRY_REQUEST.name().equals(msgType) - || SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msgType) - || DataConstants.ATTRIBUTES_UPDATED.equals(msgType) - || DataConstants.ATTRIBUTES_DELETED.equals(msgType) - || DataConstants.TIMESERIES_UPDATED.equals(msgType) - || DataConstants.ALARM.equals(msgType) - || DataConstants.CONNECT_EVENT.equals(msgType) - || DataConstants.DISCONNECT_EVENT.equals(msgType) - || DataConstants.ACTIVITY_EVENT.equals(msgType) - || DataConstants.INACTIVITY_EVENT.equals(msgType); + protected boolean isSupportedMsgType(TbMsg msg) { + return msg.isTypeOneOf(POST_TELEMETRY_REQUEST, POST_ATTRIBUTES_REQUEST, ATTRIBUTES_UPDATED, + ATTRIBUTES_DELETED, TIMESERIES_UPDATED, ALARM, CONNECT_EVENT, DISCONNECT_EVENT, ACTIVITY_EVENT, INACTIVITY_EVENT); } protected boolean isSupportedOriginator(EntityType entityType) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java index 834dbe2390..1fb000d709 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.external; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; -import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.msg.TbMsg; public abstract class TbAbstractExternalNode implements TbNode { @@ -30,7 +30,7 @@ public abstract class TbAbstractExternalNode implements TbNode { protected void tellSuccess(TbContext ctx, TbMsg tbMsg) { if (forceAck) { - ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbRelationTypes.SUCCESS); + ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbNodeConnectionType.SUCCESS); } else { ctx.tellSuccess(tbMsg); } @@ -39,13 +39,13 @@ public abstract class TbAbstractExternalNode implements TbNode { protected void tellFailure(TbContext ctx, TbMsg tbMsg, Throwable t) { if (forceAck) { if (t == null) { - ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbRelationTypes.FAILURE); + ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbNodeConnectionType.FAILURE); } else { ctx.enqueueForTellFailure(tbMsg.copyWithNewCtx(), t); } } else { if (t == null) { - ctx.tellNext(tbMsg, TbRelationTypes.FAILURE); + ctx.tellNext(tbMsg, TbNodeConnectionType.FAILURE); } else { ctx.tellFailure(tbMsg, t); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNode.java index 21e109e8f3..39c37f5c81 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNode.java @@ -31,10 +31,11 @@ import org.thingsboard.server.common.data.plugin.ComponentType; type = ComponentType.FILTER, name = "asset profile switch", customRelations = true, - relationTypes = {}, + relationTypes = {"default"}, configClazz = EmptyNodeConfiguration.class, nodeDescription = "Route incoming messages based on the name of the asset profile", - nodeDetails = "Route incoming messages based on the name of the asset profile. The asset profile name is case-sensitive", + nodeDetails = "Route incoming messages based on the name of the asset profile. The asset profile name is case-sensitive.

" + + "Output connections: Asset profile name or Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbNodeEmptyConfig") public class TbAssetTypeSwitchNode extends TbAbstractTypeSwitchNode { @@ -42,7 +43,8 @@ public class TbAssetTypeSwitchNode extends TbAbstractTypeSwitchNode { @Override protected String getRelationType(TbContext ctx, EntityId originator) throws TbNodeException { if (!EntityType.ASSET.equals(originator.getEntityType())) { - throw new TbNodeException("Unsupported originator type: " + originator.getEntityType() + "! Only 'ASSET' type is allowed."); + throw new TbNodeException("Unsupported originator type: " + originator.getEntityType().getNormalName() + "!" + + " Only " + EntityType.ASSET.getNormalName() + " type is allowed."); } AssetProfile assetProfile = ctx.getAssetProfileCache().get(ctx.getTenantId(), (AssetId) originator); if (assetProfile == null) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java index dcaf6698cd..ca58e47554 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.filter; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -28,24 +27,25 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import javax.annotation.Nullable; -import java.io.IOException; @Slf4j @RuleNode( type = ComponentType.FILTER, - name = "check alarm status", + name = "alarm status filter", configClazz = TbCheckAlarmStatusNodeConfig.class, - relationTypes = {"True", "False"}, + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, nodeDescription = "Checks alarm status.", - nodeDetails = "Checks the alarm status to match one of the specified statuses.", + nodeDetails = "Checks the alarm status to match one of the specified statuses.

" + + "Output connections: True, False, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeCheckAlarmStatusConfig") public class TbCheckAlarmStatusNode implements TbNode { + private TbCheckAlarmStatusNodeConfig config; @Override @@ -60,33 +60,24 @@ public class TbCheckAlarmStatusNode implements TbNode { ListenableFuture latest = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), alarm.getId()); - Futures.addCallback(latest, new FutureCallback() { + Futures.addCallback(latest, new FutureCallback<>() { @Override public void onSuccess(@Nullable Alarm result) { - if (result != null) { - boolean isPresent = false; - for (AlarmStatus alarmStatus : config.getAlarmStatusList()) { - if (result.getStatus() == alarmStatus) { - isPresent = true; - break; - } - } - if (isPresent) { - ctx.tellNext(msg, "True"); - } else { - ctx.tellNext(msg, "False"); - } - } else { + if (result == null) { ctx.tellFailure(msg, new TbNodeException("No such alarm found.")); + return; } + boolean isPresent = config.getAlarmStatusList().stream() + .anyMatch(alarmStatus -> result.getStatus() == alarmStatus); + ctx.tellNext(msg, isPresent ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE); } @Override public void onFailure(Throwable t) { ctx.tellFailure(msg, t); } - }, MoreExecutors.directExecutor()); - } catch (IllegalArgumentException e) { + }, ctx.getDbCallbackExecutor()); + } catch (Exception e) { log.error("Failed to parse alarm: [{}]", msg.getData()); throw new TbNodeException(e); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeConfig.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeConfig.java index d979eb10ca..4c15d635b8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeConfig.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeConfig.java @@ -24,12 +24,14 @@ import java.util.List; @Data public class TbCheckAlarmStatusNodeConfig implements NodeConfiguration { + private List alarmStatusList; @Override public TbCheckAlarmStatusNodeConfig defaultConfiguration() { - TbCheckAlarmStatusNodeConfig config = new TbCheckAlarmStatusNodeConfig(); + var config = new TbCheckAlarmStatusNodeConfig(); config.setAlarmStatusList(Arrays.asList(AlarmStatus.ACTIVE_ACK, AlarmStatus.ACTIVE_UNACK)); return config; } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java index 3853b3a62a..a5ee3b969b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java @@ -22,6 +22,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -33,12 +34,12 @@ import java.util.Map; @RuleNode( type = ComponentType.FILTER, name = "check fields presence", - relationTypes = {"True", "False"}, + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, configClazz = TbCheckMessageNodeConfiguration.class, nodeDescription = "Checks the presence of the specified fields in the message and/or metadata.", - nodeDetails = "Checks the presence of the specified fields in the message and/or metadata. " + - "By default, the rule node checks that all specified fields need to be present. " + - "Uncheck the 'Check that all specified fields are present' if the presence of at least one field is sufficient.", + nodeDetails = "By default, the rule node checks that all specified fields are present. " + + "Uncheck the 'Check that all selected fields are present' if the presence of at least one field is sufficient.

" + + "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeCheckMessageConfig") public class TbCheckMessageNode implements TbNode { @@ -59,11 +60,10 @@ public class TbCheckMessageNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { try { - if (config.isCheckAllKeys()) { - ctx.tellNext(msg, allKeysData(msg) && allKeysMetadata(msg) ? "True" : "False"); - } else { - ctx.tellNext(msg, atLeastOneData(msg) || atLeastOneMetadata(msg) ? "True" : "False"); - } + String relationType = config.isCheckAllKeys() ? + allKeysData(msg) && allKeysMetadata(msg) ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE : + atLeastOneData(msg) || atLeastOneMetadata(msg) ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE; + ctx.tellNext(msg, relationType); } catch (Exception e) { ctx.tellFailure(msg, e); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeConfiguration.java index f145be3fd2..25a6efa417 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeConfiguration.java @@ -22,7 +22,7 @@ import java.util.Collections; import java.util.List; @Data -public class TbCheckMessageNodeConfiguration implements NodeConfiguration { +public class TbCheckMessageNodeConfiguration implements NodeConfiguration { private List messageNames; private List metadataNames; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java index 4a0d6b05e7..3af5c9bfe1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java @@ -15,22 +15,26 @@ */ package org.thingsboard.rule.engine.filter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.TbVersionedNode; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import java.util.List; @@ -43,15 +47,20 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; @Slf4j @RuleNode( type = ComponentType.FILTER, - name = "check relation", + name = "check relation presence", configClazz = TbCheckRelationNodeConfiguration.class, - relationTypes = {"True", "False"}, + version = 1, + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, nodeDescription = "Checks the presence of the relation between the originator of the message and other entities.", - nodeDetails = "If 'check relation to specific entity' is selected, one must specify a related entity. " + - "Otherwise, the rule node checks the presence of a relation to any entity that matches the direction and relation type criteria.", + nodeDetails = "If 'check relation to specific entity' is selected, you should specify a related entity. " + + "Otherwise, the rule node checks the presence of a relation to any entity. " + + "In both cases, relation lookup is based on configured direction and type.

" + + "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeCheckRelationConfig") -public class TbCheckRelationNode implements TbNode { +public class TbCheckRelationNode implements TbVersionedNode { + + private static final String DIRECTION_PROPERTY_NAME = "direction"; private TbCheckRelationNodeConfiguration config; private EntityId singleEntityId; @@ -60,6 +69,9 @@ public class TbCheckRelationNode implements TbNode { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbCheckRelationNodeConfiguration.class); if (config.isCheckForSingleEntity()) { + if (StringUtils.isEmpty(config.getEntityType()) || StringUtils.isEmpty(config.getEntityId())) { + throw new TbNodeException("Entity should be specified!"); + } this.singleEntityId = EntityIdFactory.getByTypeAndId(config.getEntityType(), config.getEntityId()); ctx.checkTenantEntity(singleEntityId); } @@ -67,44 +79,56 @@ public class TbCheckRelationNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { - ListenableFuture checkRelationFuture; - if (config.isCheckForSingleEntity()) { - checkRelationFuture = processSingle(ctx, msg); - } else { - checkRelationFuture = processList(ctx, msg); - } - withCallback(checkRelationFuture, filterResult -> ctx.tellNext(msg, filterResult ? "True" : "False"), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); + ListenableFuture checkRelationFuture = config.isCheckForSingleEntity() ? + processSingle(ctx, msg) : processList(ctx, msg); + withCallback(checkRelationFuture, + filterResult -> ctx.tellNext(msg, filterResult ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE), + t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); } private ListenableFuture processSingle(TbContext ctx, TbMsg msg) { EntityId from; EntityId to; if (EntitySearchDirection.FROM.name().equals(config.getDirection())) { - from = singleEntityId; - to = msg.getOriginator(); - } else { to = singleEntityId; from = msg.getOriginator(); + } else { + from = singleEntityId; + to = msg.getOriginator(); } return ctx.getRelationService().checkRelationAsync(ctx.getTenantId(), from, to, config.getRelationType(), RelationTypeGroup.COMMON); } private ListenableFuture processList(TbContext ctx, TbMsg msg) { - if (EntitySearchDirection.FROM.name().equals(config.getDirection())) { - return Futures.transformAsync(ctx.getRelationService() - .findByToAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), config.getRelationType(), RelationTypeGroup.COMMON), this::isEmptyList, MoreExecutors.directExecutor()); - } else { - return Futures.transformAsync(ctx.getRelationService() - .findByFromAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), config.getRelationType(), RelationTypeGroup.COMMON), this::isEmptyList, MoreExecutors.directExecutor()); - } + ListenableFuture> relationListFuture = EntitySearchDirection.FROM.name().equals(config.getDirection()) ? + ctx.getRelationService().findByFromAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), config.getRelationType(), RelationTypeGroup.COMMON) : + ctx.getRelationService().findByToAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), config.getRelationType(), RelationTypeGroup.COMMON); + return Futures.transformAsync(relationListFuture, this::isEmptyList, ctx.getDbCallbackExecutor()); } private ListenableFuture isEmptyList(List entityRelations) { - if (entityRelations.isEmpty()) { - return Futures.immediateFuture(false); - } else { - return Futures.immediateFuture(true); + return entityRelations.isEmpty() ? Futures.immediateFuture(false) : Futures.immediateFuture(true); + } + + @Override + public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { + if (fromVersion == 0) { + var newConfigObjectNode = (ObjectNode) oldConfiguration; + if (!newConfigObjectNode.has(DIRECTION_PROPERTY_NAME)) { + throw new TbNodeException("property to update: '" + DIRECTION_PROPERTY_NAME + "' doesn't exists in configuration!"); + } + String direction = newConfigObjectNode.get(DIRECTION_PROPERTY_NAME).asText(); + if (EntitySearchDirection.TO.name().equals(direction)) { + newConfigObjectNode.put(DIRECTION_PROPERTY_NAME, EntitySearchDirection.FROM.name()); + return new TbPair<>(true, newConfigObjectNode); + } + if (EntitySearchDirection.FROM.name().equals(direction)) { + newConfigObjectNode.put(DIRECTION_PROPERTY_NAME, EntitySearchDirection.TO.name()); + return new TbPair<>(true, newConfigObjectNode); + } + throw new TbNodeException("property to update: '" + DIRECTION_PROPERTY_NAME + "' has invalid value!"); } + return new TbPair<>(false, oldConfiguration); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeConfiguration.java index aeda247583..1a8ab7068d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeConfiguration.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.filter; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; /** @@ -33,9 +34,9 @@ public class TbCheckRelationNodeConfiguration implements NodeConfigurationDevice profile name
or Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbNodeEmptyConfig") public class TbDeviceTypeSwitchNode extends TbAbstractTypeSwitchNode { @@ -42,7 +43,8 @@ public class TbDeviceTypeSwitchNode extends TbAbstractTypeSwitchNode { @Override protected String getRelationType(TbContext ctx, EntityId originator) throws TbNodeException { if (!EntityType.DEVICE.equals(originator.getEntityType())) { - throw new TbNodeException("Unsupported originator type: " + originator.getEntityType() + "! Only 'DEVICE' type is allowed."); + throw new TbNodeException("Unsupported originator type: " + originator.getEntityType().getNormalName() + + "! Only " + EntityType.DEVICE.getNormalName() + " type is allowed."); } DeviceProfile deviceProfile = ctx.getDeviceProfileCache().get(ctx.getTenantId(), (DeviceId) originator); if (deviceProfile == null) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java index 0bde9402a7..bcfaf78a65 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java @@ -22,6 +22,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.script.ScriptLanguage; @@ -32,7 +33,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; @Slf4j @RuleNode( type = ComponentType.FILTER, - name = "script", relationTypes = {"True", "False"}, + name = "script", + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, configClazz = TbJsFilterNodeConfiguration.class, nodeDescription = "Filter incoming messages using TBEL or JS script", nodeDetails = "Evaluates boolean function using incoming message. " + @@ -40,7 +42,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; "Script function should return boolean value and accepts three parameters:
" + "Message payload can be accessed via msg property. For example msg.temperature < 10;
" + "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';
" + - "Message type can be accessed via msgType property.", + "Message type can be accessed via msgType property.

" + + "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeScriptConfig" ) @@ -62,7 +65,7 @@ public class TbJsFilterNode implements TbNode { withCallback(scriptEngine.executeFilterAsync(msg), filterResult -> { ctx.logJsEvalResponse(); - ctx.tellNext(msg, filterResult ? "True" : "False"); + ctx.tellNext(msg, filterResult ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE); }, t -> { ctx.tellFailure(msg, t); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java index 706978846a..f55a172c02 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java @@ -44,7 +44,8 @@ import java.util.Set; "If Array is empty - message not routed to next Node. " + "Message payload can be accessed via msg property. For example msg.temperature < 10;
" + "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';
" + - "Message type can be accessed via msgType property.", + "Message type can be accessed via msgType property.

" + + "Output connections: Custom connection(s) defined by switch node or Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeSwitchConfig") public class TbJsSwitchNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java index 765deb2ea7..ca8ba9d561 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java @@ -21,6 +21,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -31,11 +32,12 @@ import org.thingsboard.server.common.msg.TbMsg; @Slf4j @RuleNode( type = ComponentType.FILTER, - name = "message type", + name = "message type filter", configClazz = TbMsgTypeFilterNodeConfiguration.class, - relationTypes = {"True", "False"}, + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, nodeDescription = "Filter incoming messages by Message Type", - nodeDetails = "If incoming MessageType is expected - send Message via True chain, otherwise False chain is used.", + nodeDetails = "If incoming message type is expected - send Message via True chain, otherwise False chain is used.

" + + "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeMessageTypeConfig") public class TbMsgTypeFilterNode implements TbNode { @@ -49,7 +51,7 @@ public class TbMsgTypeFilterNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - ctx.tellNext(msg, config.getMessageTypes().contains(msg.getType()) ? "True" : "False"); + ctx.tellNext(msg, config.getMessageTypes().contains(msg.getType()) ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java index fb326a6d50..1c0b080653 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java @@ -17,11 +17,14 @@ package org.thingsboard.rule.engine.filter; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; -import org.thingsboard.server.common.msg.session.SessionMsgType; import java.util.Arrays; import java.util.List; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; +import static org.thingsboard.server.common.data.msg.TbMsgType.TO_SERVER_RPC_REQUEST; + /** * Created by ashvayka on 19.01.18. */ @@ -32,11 +35,11 @@ public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration etc. via corresponding chain, otherwise Other chain is used.", + nodeDetails = "Sends messages with message types \"Post attributes\", \"Post telemetry\", \"RPC Request\"" + + " etc. via corresponding chain, otherwise Other chain is used.

" + + "Output connections: Message type connection, Other - if message type is custom or Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbNodeEmptyConfig") public class TbMsgTypeSwitchNode implements TbNode { @@ -52,83 +49,7 @@ public class TbMsgTypeSwitchNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - String relationType; - if (msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) { - relationType = "Post attributes"; - } else if (msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) { - relationType = "Post telemetry"; - } else if (msg.getType().equals(SessionMsgType.TO_SERVER_RPC_REQUEST.name())) { - relationType = "RPC Request from Device"; - } else if (msg.getType().equals(DataConstants.ACTIVITY_EVENT)) { - relationType = "Activity Event"; - } else if (msg.getType().equals(DataConstants.INACTIVITY_EVENT)) { - relationType = "Inactivity Event"; - } else if (msg.getType().equals(DataConstants.CONNECT_EVENT)) { - relationType = "Connect Event"; - } else if (msg.getType().equals(DataConstants.DISCONNECT_EVENT)) { - relationType = "Disconnect Event"; - } else if (msg.getType().equals(DataConstants.ENTITY_CREATED)) { - relationType = "Entity Created"; - } else if (msg.getType().equals(DataConstants.ENTITY_UPDATED)) { - relationType = "Entity Updated"; - } else if (msg.getType().equals(DataConstants.ENTITY_DELETED)) { - relationType = "Entity Deleted"; - } else if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED)) { - relationType = "Entity Assigned"; - } else if (msg.getType().equals(DataConstants.ENTITY_UNASSIGNED)) { - relationType = "Entity Unassigned"; - } else if (msg.getType().equals(DataConstants.ATTRIBUTES_UPDATED)) { - relationType = "Attributes Updated"; - } else if (msg.getType().equals(DataConstants.ATTRIBUTES_DELETED)) { - relationType = "Attributes Deleted"; - } else if (msg.getType().equals(DataConstants.ALARM_ACK)) { - relationType = "Alarm Acknowledged"; - } else if (msg.getType().equals(DataConstants.ALARM_CLEAR)) { - relationType = "Alarm Cleared"; - } else if (msg.getType().equals(DataConstants.ALARM_ASSIGNED)) { - relationType = "Alarm Assigned"; - } else if (msg.getType().equals(DataConstants.ALARM_UNASSIGNED)) { - relationType = "Alarm Unassigned"; - } else if (msg.getType().equals(DataConstants.COMMENT_CREATED)) { - relationType = "Comment Created"; - } else if (msg.getType().equals(DataConstants.COMMENT_UPDATED)) { - relationType = "Comment Updated"; - } else if (msg.getType().equals(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE)) { - relationType = "RPC Request to Device"; - } else if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED_FROM_TENANT)) { - relationType = "Entity Assigned From Tenant"; - } else if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED_TO_TENANT)) { - relationType = "Entity Assigned To Tenant"; - } else if (msg.getType().equals(DataConstants.TIMESERIES_UPDATED)) { - relationType = "Timeseries Updated"; - } else if (msg.getType().equals(DataConstants.TIMESERIES_DELETED)) { - relationType = "Timeseries Deleted"; - } else if (msg.getType().equals(DataConstants.RPC_QUEUED)) { - relationType = "RPC Queued"; - } else if (msg.getType().equals(DataConstants.RPC_SENT)) { - relationType = "RPC Sent"; - } else if (msg.getType().equals(DataConstants.RPC_DELIVERED)) { - relationType = "RPC Delivered"; - } else if (msg.getType().equals(DataConstants.RPC_SUCCESSFUL)) { - relationType = "RPC Successful"; - } else if (msg.getType().equals(DataConstants.RPC_TIMEOUT)) { - relationType = "RPC Timeout"; - } else if (msg.getType().equals(DataConstants.RPC_EXPIRED)) { - relationType = "RPC Expired"; - } else if (msg.getType().equals(DataConstants.RPC_FAILED)) { - relationType = "RPC Failed"; - } else if (msg.getType().equals(DataConstants.RPC_DELETED)) { - relationType = "RPC Deleted"; - } else if (msg.getType().equals(DataConstants.RELATION_ADD_OR_UPDATE)) { - relationType = "Relation Added or Updated"; - } else if (msg.getType().equals(DataConstants.RELATION_DELETED)) { - relationType = "Relation Deleted"; - } else if (msg.getType().equals(DataConstants.RELATIONS_DELETED)) { - relationType = "All Relations Deleted"; - } else { - relationType = "Other"; - } - ctx.tellNext(msg, relationType); + ctx.tellNext(msg, msg.getInternalType().getRuleNodeConnection()); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java index f4e13d761e..183721e72a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java @@ -21,6 +21,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -29,11 +30,12 @@ import org.thingsboard.server.common.msg.TbMsg; @Slf4j @RuleNode( type = ComponentType.FILTER, - name = "entity type", + name = "entity type filter", configClazz = TbOriginatorTypeFilterNodeConfiguration.class, - relationTypes = {"True", "False"}, + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, nodeDescription = "Filter incoming messages by the type of message originator entity", - nodeDetails = "Checks that the entity type of the incoming message originator matches one of the values specified in the filter.", + nodeDetails = "Checks that the entity type of the incoming message originator matches one of the values specified in the filter.

" + + "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeOriginatorTypeConfig") public class TbOriginatorTypeFilterNode implements TbNode { @@ -48,7 +50,7 @@ public class TbOriginatorTypeFilterNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { EntityType originatorType = msg.getOriginator().getEntityType(); - ctx.tellNext(msg, config.getOriginatorTypes().contains(originatorType) ? "True" : "False"); + ctx.tellNext(msg, config.getOriginatorTypes().contains(originatorType) ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java index a920af5066..17b7d575f5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java @@ -19,8 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -29,55 +27,17 @@ import org.thingsboard.server.common.data.plugin.ComponentType; type = ComponentType.FILTER, name = "entity type switch", configClazz = EmptyNodeConfiguration.class, - relationTypes = {"Device", "Asset", "Alarm", "Entity View", "Tenant", "Customer", "User", "Dashboard", "Rule chain", "Rule node", "Edge"}, + relationTypes = {}, // should always be empty. We add the relation types for this node in AnnotationComponentDiscoveryService. nodeDescription = "Route incoming messages by Message Originator Type", - nodeDetails = "Routes messages to chain according to the entity type ('Device', 'Asset', etc.).", + nodeDetails = "Routes messages to chain according to the entity type ('Device', 'Asset', etc.).

" + + "Output connections: Message originator type or Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbNodeEmptyConfig") public class TbOriginatorTypeSwitchNode extends TbAbstractTypeSwitchNode { @Override - protected String getRelationType(TbContext ctx, EntityId originator) throws TbNodeException { - String relationType; - EntityType originatorType = originator.getEntityType(); - switch (originatorType) { - case TENANT: - relationType = "Tenant"; - break; - case CUSTOMER: - relationType = "Customer"; - break; - case USER: - relationType = "User"; - break; - case DASHBOARD: - relationType = "Dashboard"; - break; - case ASSET: - relationType = "Asset"; - break; - case DEVICE: - relationType = "Device"; - break; - case ENTITY_VIEW: - relationType = "Entity View"; - break; - case EDGE: - relationType = "Edge"; - break; - case RULE_CHAIN: - relationType = "Rule chain"; - break; - case RULE_NODE: - relationType = "Rule node"; - break; - case ALARM: - relationType = "Alarm"; - break; - default: - throw new TbNodeException("Unsupported originator type: " + originatorType); - } - return relationType; + protected String getRelationType(TbContext ctx, EntityId originator) { + return originator.getEntityType().getNormalName(); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java index 35baaf461b..a92cafefd3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java @@ -21,7 +21,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -48,7 +48,7 @@ public class TbCheckpointNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - ctx.enqueueForTellNext(msg, queueName, TbRelationTypes.SUCCESS, () -> ctx.ack(msg), error -> ctx.tellFailure(msg, error)); + ctx.enqueueForTellNext(msg, queueName, TbNodeConnectionType.SUCCESS, () -> ctx.ack(msg), error -> ctx.tellFailure(msg, error)); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java index 18d817fd94..c55113783c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java @@ -29,7 +29,6 @@ import com.google.pubsub.v1.PubsubMessage; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; @@ -105,28 +104,28 @@ public class TbPubSubNode extends TbAbstractExternalNode { ApiFuture messageIdFuture = this.pubSubClient.publish(pubsubMessageBuilder.build()); ApiFutures.addCallback(messageIdFuture, new ApiFutureCallback() { public void onSuccess(String messageId) { - TbMsg next = processPublishResult(ctx, msg, messageId); + TbMsg next = processPublishResult(msg, messageId); tellSuccess(ctx, next); } public void onFailure(Throwable t) { - TbMsg next = processException(ctx, msg, t); + TbMsg next = processException(msg, t); tellFailure(ctx, next, t); } }, ctx.getExternalCallExecutor()); } - private TbMsg processPublishResult(TbContext ctx, TbMsg origMsg, String messageId) { + private TbMsg processPublishResult(TbMsg origMsg, String messageId) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(MESSAGE_ID, messageId); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) { + private TbMsg processException(TbMsg origMsg, Throwable t) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } private Publisher initPubSubClient() throws IOException { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java index 1f1f1fd132..b263518c41 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java @@ -48,14 +48,14 @@ public abstract class AbstractGeofencingNode getConfigClazz(); protected boolean checkMatches(TbMsg msg) throws TbNodeException { - JsonElement msgDataElement = new JsonParser().parse(msg.getData()); + JsonElement msgDataElement = JsonParser.parseString(msg.getData()); if (!msgDataElement.isJsonObject()) { - throw new TbNodeException("Incoming Message is not a valid JSON object"); + throw new TbNodeException("Incoming Message is not a valid JSON object!"); } JsonObject msgDataObj = msgDataElement.getAsJsonObject(); double latitude = getValueFromMessageByName(msg, msgDataObj, config.getLatitudeKeyName()); double longitude = getValueFromMessageByName(msg, msgDataObj, config.getLongitudeKeyName()); - List perimeters = getPerimeters(msg, msgDataObj); + List perimeters = getPerimeters(msg); boolean matches = false; for (Perimeter perimeter : perimeters) { if (checkMatches(perimeter, latitude, longitude)) { @@ -74,11 +74,11 @@ public abstract class AbstractGeofencingNode getPerimeters(TbMsg msg, JsonObject msgDataObj) throws TbNodeException { + protected List getPerimeters(TbMsg msg) throws TbNodeException { if (config.isFetchPerimeterInfoFromMessageMetadata()) { if (StringUtils.isEmpty(config.getPerimeterKeyName())) { // Old configuration before "perimeterKeyName" was introduced diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java index 519a4274c1..4467dfe30b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java @@ -45,8 +45,6 @@ public class GeoUtil { private static final SpatialContext distCtx = SpatialContext.GEO; private static final JtsSpatialContext jtsCtx; - private static final JsonParser JSON_PARSER = new JsonParser(); - static { JtsSpatialContextFactory factory = new JtsSpatialContextFactory(); factory.normWrapLongitude = true; @@ -64,7 +62,7 @@ public class GeoUtil { throw new RuntimeException("Polygon string can't be empty or null!"); } - JsonArray polygonsJson = normalizePolygonsJson(JSON_PARSER.parse(polygonInString).getAsJsonArray()); + JsonArray polygonsJson = normalizePolygonsJson(JsonParser.parseString(polygonInString).getAsJsonArray()); List polygons = buildPolygonsFromJson(polygonsJson); Set holes = extractHolesFrom(polygons); polygons.removeIf(holes::contains); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java index 373118ebdd..20e7039be4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -30,7 +31,7 @@ import org.thingsboard.server.common.msg.TbMsg; type = ComponentType.FILTER, name = "gps geofencing filter", configClazz = TbGpsGeofencingFilterNodeConfiguration.class, - relationTypes = {"True", "False"}, + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, nodeDescription = "Filter incoming messages by GPS based geofencing", nodeDetails = "Extracts latitude and longitude parameters from the incoming message and checks them according to configured perimeter.
" + "Configuration:

" + @@ -57,14 +58,15 @@ import org.thingsboard.server.common.msg.TbMsg; "

" + "{\"latitude\": 48.198618758582384, \"longitude\": 24.65322245153503, \"radius\": 100.0, \"radiusUnit\": \"METER\" }" + "

" + - "Available radius units: METER, KILOMETER, FOOT, MILE, NAUTICAL_MILE;", + "Available radius units: METER, KILOMETER, FOOT, MILE, NAUTICAL_MILE;

" + + "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeGpsGeofencingConfig") public class TbGpsGeofencingFilterNode extends AbstractGeofencingNode { @Override public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { - ctx.tellNext(msg, checkMatches(msg) ? "True" : "False"); + ctx.tellNext(msg, checkMatches(msg) ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java index 760eec2421..68094196d0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java @@ -28,10 +28,8 @@ import org.apache.kafka.common.header.internals.RecordHeaders; import org.springframework.util.ReflectionUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.exception.ThingsboardKafkaClientError; @@ -167,24 +165,24 @@ public class TbKafkaNode extends TbAbstractExternalNode { private void processRecord(TbContext ctx, TbMsg msg, RecordMetadata metadata, Exception e) { if (e == null) { - tellSuccess(ctx, processResponse(ctx, msg, metadata)); + tellSuccess(ctx, processResponse(msg, metadata)); } else { - tellFailure(ctx, processException(ctx, msg, e), e); + tellFailure(ctx, processException(msg, e), e); } } - private TbMsg processResponse(TbContext ctx, TbMsg origMsg, RecordMetadata recordMetadata) { + private TbMsg processResponse(TbMsg origMsg, RecordMetadata recordMetadata) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(OFFSET, String.valueOf(recordMetadata.offset())); metaData.putValue(PARTITION, String.valueOf(recordMetadata.partition())); metaData.putValue(TOPIC, recordMetadata.topic()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Exception e) { + private TbMsg processException(TbMsg origMsg, Exception e) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java index 2af4978766..a52641bedc 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbEmail; @@ -28,6 +27,8 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -35,9 +36,6 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; -import static org.thingsboard.rule.engine.mail.TbSendEmailNode.SEND_EMAIL_TYPE; - @Slf4j @RuleNode( type = ComponentType.TRANSFORMATION, @@ -69,7 +67,7 @@ public class TbMsgToEmailNode implements TbNode { try { TbEmail email = convert(msg); TbMsg emailMsg = buildEmailMsg(ctx, msg, email); - ctx.tellNext(emailMsg, SUCCESS); + ctx.tellNext(emailMsg, TbNodeConnectionType.SUCCESS); } catch (Exception ex) { log.warn("Can not convert message to email " + ex.getMessage()); ctx.tellFailure(msg, ex); @@ -78,7 +76,7 @@ public class TbMsgToEmailNode implements TbNode { private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, TbEmail email) throws JsonProcessingException { String emailJson = JacksonUtil.toString(email); - return ctx.transformMsg(msg, SEND_EMAIL_TYPE, msg.getOriginator(), msg.getMetaData().copy(), emailJson); + return ctx.transformMsg(msg, TbMsgType.SEND_EMAIL, msg.getOriginator(), msg.getMetaData().copy(), emailJson); } private TbEmail convert(TbMsg msg) throws IOException { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java index 3e2d96b8f4..2f7aaf257d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java @@ -16,9 +16,8 @@ package org.thingsboard.rule.engine.mail; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.StringUtils; import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbEmail; @@ -26,6 +25,8 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -50,7 +51,6 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; public class TbSendEmailNode extends TbAbstractExternalNode { private static final String MAIL_PROP = "mail."; - static final String SEND_EMAIL_TYPE = "SEND_EMAIL"; private TbSendEmailNodeConfiguration config; private JavaMailSenderImpl mailSender; @@ -70,7 +70,7 @@ public class TbSendEmailNode extends TbAbstractExternalNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { try { - validateType(msg.getType()); + validateType(msg); TbEmail email = getEmail(msg); var tbMsg = ackIfNeeded(ctx, msg); withCallback(ctx.getMailExecutor().executeAsync(() -> { @@ -100,8 +100,9 @@ public class TbSendEmailNode extends TbAbstractExternalNode { return email; } - private void validateType(String type) { - if (!SEND_EMAIL_TYPE.equals(type)) { + private void validateType(TbMsg msg) { + if (!msg.isTypeOf(TbMsgType.SEND_EMAIL)) { + String type = msg.getType(); log.warn("Not expected msg type [{}] for SendEmail Node", type); throw new IllegalStateException("Not expected msg type " + type + " for SendEmail Node"); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java index 33692decc2..f7b4eb163c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import lombok.Data; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.objecthunter.exp4j.Expression; import net.objecthunter.exp4j.ExpressionBuilder; @@ -44,9 +46,10 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -81,9 +84,8 @@ import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT; ) public class TbMathNode implements TbNode { - private static final ConcurrentMap semaphores = new ConcurrentReferenceHashMap<>(); + private static final ConcurrentMap> locks = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); private final ThreadLocal customExpression = new ThreadLocal<>(); - private TbMathNodeConfiguration config; private boolean msgBodyToJsonConversionRequired; @@ -108,51 +110,69 @@ public class TbMathNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - var originator = msg.getOriginator(); - var originatorSemaphore = semaphores.computeIfAbsent(originator, tmp -> new Semaphore(1, true)); - boolean acquired = tryAcquire(originator, originatorSemaphore); + var semaphoreWithQueue = locks.computeIfAbsent(msg.getOriginator(), SemaphoreWithQueue::new); + semaphoreWithQueue.getQueue().add(new TbMsgTbContext(msg, ctx)); - if (!acquired) { - ctx.tellFailure(msg, new RuntimeException("Failed to process message for originator synchronously")); - return; - } + tryProcessQueue(semaphoreWithQueue); + } - try { - var arguments = config.getArguments(); - Optional msgBodyOpt = convertMsgBodyIfRequired(msg); - var argumentValues = Futures.allAsList(arguments.stream() - .map(arg -> resolveArguments(ctx, msg, msgBodyOpt, arg)).collect(Collectors.toList())); - ListenableFuture resultMsgFuture = Futures.transformAsync(argumentValues, args -> - updateMsgAndDb(ctx, msg, msgBodyOpt, calculateResult(args)), ctx.getDbCallbackExecutor()); - DonAsynchron.withCallback(resultMsgFuture, resultMsg -> { - try { - ctx.tellSuccess(resultMsg); - } finally { - originatorSemaphore.release(); + void tryProcessQueue(SemaphoreWithQueue lockAndQueue) { + final Semaphore semaphore = lockAndQueue.getSemaphore(); + final Queue queue = lockAndQueue.getQueue(); + while (!queue.isEmpty()) { + // The semaphore have to be acquired before EACH poll and released before NEXT poll. + // Otherwise, some message will remain unprocessed in queue + if (!semaphore.tryAcquire()) { + return; + } + TbMsgTbContext tbMsgTbContext = null; + try { + tbMsgTbContext = queue.poll(); + if (tbMsgTbContext == null) { + semaphore.release(); + continue; } - }, t -> { - try { - ctx.tellFailure(msg, t); - } finally { - originatorSemaphore.release(); + final TbMsg msg = tbMsgTbContext.getMsg(); + if (!msg.getCallback().isMsgValid()) { + log.trace("[{}] Skipping non-valid message [{}]", lockAndQueue.getEntityId(), msg); + semaphore.release(); + continue; } - }, ctx.getDbCallbackExecutor()); - } catch (Throwable e) { - originatorSemaphore.release(); - log.warn("[{}] Failed to process message: {}", originator, msg, e); - throw e; + //DO PROCESSING + final TbContext ctx = tbMsgTbContext.getCtx(); + final ListenableFuture resultMsgFuture = processMsgAsync(ctx, msg); + DonAsynchron.withCallback(resultMsgFuture, resultMsg -> { + try { + ctx.tellSuccess(resultMsg); + } finally { + lockAndQueue.getSemaphore().release(); + tryProcessQueue(lockAndQueue); + } + }, t -> { + try { + ctx.tellFailure(msg, t); + } finally { + lockAndQueue.getSemaphore().release(); + tryProcessQueue(lockAndQueue); + } + }, ctx.getDbCallbackExecutor()); + } catch (Throwable e) { + semaphore.release(); + log.warn("[{}] Failed to process message: {}", lockAndQueue.getEntityId(), tbMsgTbContext == null ? null : tbMsgTbContext.getMsg(), e); + throw e; + } + break; //submitted async exact one task. next poll will try on callback } } - private boolean tryAcquire(EntityId originator, Semaphore originatorSemaphore) { - boolean acquired; - try { - acquired = originatorSemaphore.tryAcquire(20, TimeUnit.SECONDS); - } catch (InterruptedException e) { - acquired = false; - log.debug("[{}] Failed to acquire semaphore", originator, e); - } - return acquired; + ListenableFuture processMsgAsync(TbContext ctx, TbMsg msg) { + var arguments = config.getArguments(); + Optional msgBodyOpt = convertMsgBodyIfRequired(msg); + var argumentValues = Futures.allAsList(arguments.stream() + .map(arg -> resolveArguments(ctx, msg, msgBodyOpt, arg)).collect(Collectors.toList())); + ListenableFuture resultMsgFuture = Futures.transformAsync(argumentValues, args -> + updateMsgAndDb(ctx, msg, msgBodyOpt, calculateResult(args)), ctx.getDbCallbackExecutor()); + return resultMsgFuture; } private ListenableFuture updateMsgAndDb(TbContext ctx, TbMsg msg, Optional msgBodyOpt, double result) { @@ -248,7 +268,7 @@ public class TbMathNode implements TbNode { } else { md.putValue(mathResultKey, Double.toString(toDoubleValue(mathResultDef, result))); } - return TbMsg.transformMsg(msg, md); + return TbMsg.transformMsgMetadata(msg, md); } private double calculateResult(List args) { @@ -402,4 +422,20 @@ public class TbMathNode implements TbNode { @Override public void destroy() { } + + @Data + @RequiredArgsConstructor + static public class SemaphoreWithQueue { + final EntityId entityId; + final Semaphore semaphore = new Semaphore(1); + final Queue queue = new ConcurrentLinkedQueue<>(); + } + + @Data + @RequiredArgsConstructor + static public class TbMsgTbContext { + final TbMsg msg; + final TbContext ctx; + } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNodeConfiguration.java index 1636898b8c..f4fccd3ae6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNodeConfiguration.java @@ -32,9 +32,10 @@ public class TbMathNodeConfiguration implements NodeConfigurationSuccess, Other or Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeCalculateDeltaConfig") public class CalculateDeltaNode implements TbNode { @@ -73,14 +75,14 @@ public class CalculateDeltaNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (!msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) { - ctx.tellNext(msg, "Other"); + if (!msg.isTypeOf(TbMsgType.POST_TELEMETRY_REQUEST)) { + ctx.tellNext(msg, TbNodeConnectionType.OTHER); return; } JsonNode json = JacksonUtil.toJsonNode(msg.getData()); String inputKey = config.getInputValueKey(); if (!json.has(inputKey)) { - ctx.tellNext(msg, "Other"); + ctx.tellNext(msg, TbNodeConnectionType.OTHER); return; } withCallback(getLastValue(msg.getOriginator()), @@ -114,7 +116,7 @@ public class CalculateDeltaNode implements TbNode { long period = previousData != null ? currentTs - previousData.ts : 0; result.put(config.getPeriodValueKey(), period); } - ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(result))); + ctx.tellSuccess(TbMsg.transformMsgData(msg, JacksonUtil.toString(result))); }, t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractFetchToNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractFetchToNodeConfiguration.java index 14c27a7ac7..2c14d1bea9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractFetchToNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractFetchToNodeConfiguration.java @@ -16,10 +16,11 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; +import org.thingsboard.rule.engine.util.TbMsgSource; @Data public abstract class TbAbstractFetchToNodeConfiguration { - private FetchTo fetchTo; + private TbMsgSource fetchTo; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java index ffa735fd4f..3ec1fd4921 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.metadata; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -27,6 +28,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -66,7 +68,7 @@ public abstract class TbAbstractGetAttributesNode safePutAttributes(ctx, msg, msgDataAsObjectNode, entityId), @@ -75,6 +77,20 @@ public abstract class TbAbstractGetAttributesNode findEntityIdAsync(TbContext ctx, TbMsg msg); + protected TbPair upgradeRuleNodesWithOldPropertyToUseFetchTo( + JsonNode oldConfiguration, + String oldProperty, + String ifTrue, + String ifFalse + ) throws TbNodeException { + var newConfigObjectNode = (ObjectNode) oldConfiguration; + if (!newConfigObjectNode.has(oldProperty)) { + newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, TbMsgSource.METADATA.name()); + return new TbPair<>(true, newConfigObjectNode); + } + return upgradeConfigurationToUseFetchTo(oldProperty, ifTrue, ifFalse, newConfigObjectNode); + } + private void safePutAttributes(TbContext ctx, TbMsg msg, ObjectNode msgDataNode, T entityId) { Set>> failuresPairSet = ConcurrentHashMap.newKeySet(); var getKvEntryPairFutures = Futures.allAsList( @@ -150,7 +166,7 @@ public abstract class TbAbstractGetAttributesNode extends Tb @Override public void onMsg(TbContext ctx, TbMsg msg) { - var msgDataAsObjectNode = FetchTo.DATA.equals(fetchTo) ? getMsgDataAsObjectNode(msg) : null; + var msgDataAsObjectNode = TbMsgSource.DATA.equals(fetchTo) ? getMsgDataAsObjectNode(msg) : null; withCallback(findEntityAsync(ctx, msg.getOriginator()), entityId -> processDataAndTell(ctx, msg, entityId, msgDataAsObjectNode), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); @@ -89,7 +90,7 @@ public abstract class TbAbstractGetEntityDataNode extends Tb } else { throw new TbNodeException("property to update: '" + OLD_DATA_TO_FETCH_PROPERTY_NAME + "' has unexpected value: " + value + ". Allowed values: true or false!"); } - newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, FetchTo.METADATA.name()); + newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, TbMsgSource.METADATA.name()); return new TbPair<>(true, newConfigObjectNode); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java index 14227cadc4..e9b0cecd1a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.util.ContactBasedEntityDetails; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.ContactBased; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.msg.TbMsg; @@ -37,7 +38,7 @@ public abstract class TbAbstractGetEntityDetailsNode ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); @@ -49,7 +50,7 @@ public abstract class TbAbstractGetEntityDetailsNode detailsList) throws TbNodeException { if (detailsList == null || detailsList.isEmpty()) { - throw new TbNodeException("No entity details selected!"); + throw new TbNodeException("At least one entity detail should be selected!"); } } @@ -114,9 +115,9 @@ public abstract class TbAbstractGetEntityDetailsNode upgradeConfigurationToUseFetchTo( + String oldProperty, String ifTrue, + String ifFalse, ObjectNode newConfig + ) throws TbNodeException { + var value = newConfig.get(oldProperty).asText(); if ("true".equals(value)) { - newConfigObjectNode.remove(oldProperty); - newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, ifTrue); - return new TbPair<>(true, newConfigObjectNode); + newConfig.remove(oldProperty); + newConfig.put(FETCH_TO_PROPERTY_NAME, ifTrue); + return new TbPair<>(true, newConfig); } else if ("false".equals(value)) { - newConfigObjectNode.remove(oldProperty); - newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, ifFalse); - return new TbPair<>(true, newConfigObjectNode); + newConfig.remove(oldProperty); + newConfig.put(FETCH_TO_PROPERTY_NAME, ifFalse); + return new TbPair<>(true, newConfig); } else { - throw new TbNodeException("property to update: '" + oldProperty + "' has unexpected value: " + value + ". Allowed values: true or false!"); + throw new TbNodeException("property to update: '" + oldProperty + "' has unexpected value: " + + value + ". Allowed values: true or false!"); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java index e23da1e364..c4770d625f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java @@ -23,6 +23,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -41,7 +42,9 @@ import java.util.concurrent.ExecutionException; nodeDescription = "Adds device credentials to the message or message metadata", nodeDetails = "if message originator type is Device and device credentials was successfully fetched, " + "rule node enriches message or message metadata with credentialsType and credentials properties. " + - "Useful when you need to fetch device credentials and use them for further message processing. For example, use device credentials to interact with external systems.", + "Useful when you need to fetch device credentials and use them for further message processing. " + + "For example, use device credentials to interact with external systems.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeFetchDeviceCredentialsConfig") public class TbFetchDeviceCredentialsNode extends TbAbstractNodeWithFetchTo { @@ -57,7 +60,7 @@ public class TbFetchDeviceCredentialsNode extends TbAbstractNodeWithFetchTo(false, oldConfiguration); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java index 038e6c1c63..70712b4370 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; @Data @EqualsAndHashCode(callSuper = true) @@ -28,7 +29,7 @@ public class TbFetchDeviceCredentialsNodeConfiguration extends TbAbstractFetchTo @Override public TbFetchDeviceCredentialsNodeConfiguration defaultConfiguration() { var configuration = new TbFetchDeviceCredentialsNodeConfiguration(); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java index 975da84510..9fbe5eb666 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java @@ -24,6 +24,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; @@ -40,7 +41,8 @@ import org.thingsboard.server.common.msg.TbMsg; nodeDescription = "Adds attributes and/or latest timeseries data for the message originator to the message or message metadata", nodeDetails = "Useful when you need to retrieve some attributes or the latest telemetry readings from the message originator " + "that are not included in the incoming message to use them for further message processing. " + - "For example to filter messages based on the threshold value stored in the attributes.", + "For example to filter messages based on the threshold value stored in the attributes.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeOriginatorAttributesConfig") public class TbGetAttributesNode extends TbAbstractGetAttributesNode { @@ -61,8 +63,8 @@ public class TbGetAttributesNode extends TbAbstractGetAttributesNode(false, oldConfiguration); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java index 2450796b59..fe15ff817f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; import java.util.Collections; import java.util.List; @@ -47,7 +48,7 @@ public class TbGetAttributesNodeConfiguration extends TbAbstractFetchToNodeConfi configuration.setLatestTsKeyNames(Collections.emptyList()); configuration.setTellFailureIfAbsent(true); configuration.setGetLatestValueWithTs(false); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java index 3cf69b5e26..088aa596fc 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java @@ -39,7 +39,8 @@ import org.thingsboard.server.common.data.util.TbPair; nodeDescription = "Adds message originator customer attributes or latest telemetry into message or message metadata", nodeDetails = "Useful in multi-customer solutions where each customer has a different configuration or threshold set " + "that is stored as customer attributes or telemetry data and used for dynamic message filtering, transformation, " + - "or actions such as alarm creation if the threshold is exceeded.", + "or actions such as alarm creation if the threshold is exceeded.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeCustomerAttributesConfig") public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode { @@ -64,9 +65,7 @@ public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { - return fromVersion == 0 ? - upgradeToUseFetchToAndDataToFetch(oldConfiguration) : - new TbPair<>(false, oldConfiguration); + return fromVersion == 0 ? upgradeToUseFetchToAndDataToFetch(oldConfiguration) : new TbPair<>(false, oldConfiguration); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java index 3be5ac15c3..f0b391ca54 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java @@ -24,6 +24,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasName; @@ -47,7 +48,8 @@ import java.util.NoSuchElementException; version = 1, nodeDescription = "Adds message originator customer details into message or message metadata", nodeDetails = "Useful in multi-customer solutions where we need dynamically use customer contact information " + - "such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.", + "such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeEntityDetailsConfig") public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode { @@ -111,8 +113,8 @@ public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode(false, oldConfiguration); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeConfiguration.java index c49a1d3c30..370e7cd5a3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; import java.util.Collections; @@ -29,7 +30,7 @@ public class TbGetCustomerDetailsNodeConfiguration extends TbAbstractGetEntityDe public TbGetCustomerDetailsNodeConfiguration defaultConfiguration() { var configuration = new TbGetCustomerDetailsNodeConfiguration(); configuration.setDetailsList(Collections.emptyList()); - configuration.setFetchTo(FetchTo.DATA); + configuration.setFetchTo(TbMsgSource.DATA); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java index 67e544fcfa..ac88005f51 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java @@ -25,6 +25,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.util.EntitiesRelatedDeviceIdAsyncLoader; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; @@ -38,7 +39,9 @@ import org.thingsboard.server.common.msg.TbMsg; nodeDescription = "Add originators related device attributes and/or latest telemetry values into message or message metadata", nodeDetails = "Related device lookup based on the configured relation query. " + "If multiple related devices are found, only first device is used for message enrichment, other entities are discarded. " + - "Useful when you need to retrieve attributes and/or latest telemetry values from device that has a relation to the message originator and use them for further message processing.", + "Useful when you need to retrieve attributes and/or latest telemetry values from device that has a relation " + + "to the message originator and use them for further message processing.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeDeviceAttributesConfig") public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode { @@ -64,8 +67,8 @@ public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode(false, oldConfiguration); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java index 60a2c9cdf7..e6be83eb0c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.data.DeviceRelationsQuery; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -38,7 +39,7 @@ public class TbGetDeviceAttrNodeConfiguration extends TbGetAttributesNodeConfigu configuration.setLatestTsKeyNames(Collections.emptyList()); configuration.setTellFailureIfAbsent(true); configuration.setGetLatestValueWithTs(false); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); var deviceRelationsQuery = new DeviceRelationsQuery(); deviceRelationsQuery.setDirection(EntitySearchDirection.FROM); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityDataNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityDataNodeConfiguration.java index eb3f3b805c..ec57cba3d2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityDataNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityDataNodeConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; import java.util.HashMap; @@ -34,7 +35,7 @@ public class TbGetEntityDataNodeConfiguration extends TbGetMappedDataNodeConfigu dataMapping.putIfAbsent("alarmThreshold", "threshold"); configuration.setDataMapping(dataMapping); configuration.setDataToFetch(DataToFetch.ATTRIBUTES); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java index 45ca8e19c0..6c9f78e9b4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; import java.util.HashMap; import java.util.Map; @@ -37,7 +38,7 @@ public class TbGetOriginatorFieldsConfiguration extends TbGetMappedDataNodeConfi dataMapping.put("type", "originatorType"); configuration.setDataMapping(dataMapping); configuration.setIgnoreNullStrings(false); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java index 34153360a3..cb7877d99a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java @@ -23,6 +23,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; @@ -40,7 +41,8 @@ import java.util.concurrent.ExecutionException; version = 1, nodeDescription = "Adds message originator fields values into message or message metadata", nodeDetails = "Fetches fields values specified in the mapping. If specified field is not part of originator fields it will be ignored. " + - "Useful when you need to retrieve originator fields and use them for further message processing.", + "Useful when you need to retrieve originator fields and use them for further message processing.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeOriginatorFieldsConfig") public class TbGetOriginatorFieldsNode extends TbAbstractGetMappedDataNode { @@ -57,7 +59,7 @@ public class TbGetOriginatorFieldsNode extends TbAbstractGetMappedDataNode(true, newConfigObjectNode); } return new TbPair<>(false, oldConfiguration); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java index 249caa87ac..50e75c8cc0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java @@ -40,7 +40,8 @@ import java.util.Arrays; nodeDescription = "Adds originators related entity attributes or latest telemetry or fields into message or message metadata", nodeDetails = "Related entity lookup based on the configured relation query. " + "If multiple related entities are found, only first entity is used for message enrichment, other entities are discarded. " + - "Useful when you need to retrieve data from an entity that has a relation to the message originator and use them for further message processing.", + "Useful when you need to retrieve data from an entity that has a relation to the message originator and use them for further message processing.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeRelatedAttributesConfig") public class TbGetRelatedAttributeNode extends TbAbstractGetEntityDataNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDataNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDataNodeConfiguration.java index 4c78915ae4..4cb0723365 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDataNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDataNodeConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.data.RelationsQuery; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; @@ -38,7 +39,7 @@ public class TbGetRelatedDataNodeConfiguration extends TbGetEntityDataNodeConfig dataMapping.putIfAbsent("serialNumber", "sn"); configuration.setDataMapping(dataMapping); configuration.setDataToFetch(DataToFetch.ATTRIBUTES); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); var relationsQuery = new RelationsQuery(); var relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java index 569c76c2a2..1bf1b07530 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java @@ -37,16 +37,13 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL; -import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_FIRST; -import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE; - /** * Created by mshvayka on 04.09.18. */ @@ -57,7 +54,8 @@ import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfigurati nodeDescription = "Adds message originator telemetry for selected time range into message metadata", nodeDetails = "Useful when you need to get telemetry data set from the message originator for a specific time range " + "instead of fetching just the latest telemetry or if you need to get the closest telemetry to the fetch interval start or end. " + - "Also, this node can be used for telemetry aggregation within configured fetch interval.", + "Also, this node can be used for telemetry aggregation within configured fetch interval.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase") public class TbGetTelemetryNode implements TbNode { @@ -76,7 +74,7 @@ public class TbGetTelemetryNode implements TbNode { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbGetTelemetryNodeConfiguration.class); tsKeyNames = config.getLatestTsKeyNames(); - limit = config.getFetchMode().equals(FETCH_MODE_ALL) ? validateLimit(config.getLimit()) : 1; + limit = config.getFetchMode().equals(TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL) ? validateLimit(config.getLimit()) : 1; fetchMode = config.getFetchMode(); orderByFetchAll = config.getOrderBy(); if (StringUtils.isEmpty(orderByFetchAll)) { @@ -86,7 +84,7 @@ public class TbGetTelemetryNode implements TbNode { } Aggregation parseAggregationConfig(String aggName) { - if (StringUtils.isEmpty(aggName) || !fetchMode.equals(FETCH_MODE_ALL)) { + if (StringUtils.isEmpty(aggName) || !fetchMode.equals(TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL)) { return Aggregation.NONE; } return Aggregation.valueOf(aggName); @@ -102,8 +100,8 @@ public class TbGetTelemetryNode implements TbNode { List keys = TbNodeUtils.processPatterns(tsKeyNames, msg); ListenableFuture> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), buildQueries(interval, keys)); DonAsynchron.withCallback(list, data -> { - process(data, msg, keys); - ctx.tellSuccess(ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), msg.getData())); + var metaData = updateMetadata(data, msg, keys); + ctx.tellSuccess(TbMsg.transformMsgMetadata(msg, metaData)); }, error -> ctx.tellFailure(msg, error), ctx.getDbCallbackExecutor()); } catch (Exception e) { ctx.tellFailure(msg, e); @@ -124,28 +122,29 @@ public class TbGetTelemetryNode implements TbNode { private String getOrderBy() { switch (fetchMode) { - case FETCH_MODE_ALL: + case TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL: return orderByFetchAll; - case FETCH_MODE_FIRST: + case TbGetTelemetryNodeConfiguration.FETCH_MODE_FIRST: return ASC_ORDER; default: return DESC_ORDER; } } - private void process(List entries, TbMsg msg, List keys) { + private TbMsgMetaData updateMetadata(List entries, TbMsg msg, List keys) { ObjectNode resultNode = JacksonUtil.newObjectNode(JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER); - if (FETCH_MODE_ALL.equals(fetchMode)) { + if (TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL.equals(fetchMode)) { entries.forEach(entry -> processArray(resultNode, entry)); } else { entries.forEach(entry -> processSingle(resultNode, entry)); } - + var copy = msg.getMetaData().copy(); for (String key : keys) { if (resultNode.has(key)) { - msg.getMetaData().putValue(key, resultNode.get(key).toString()); + copy.putValue(key, resultNode.get(key).toString()); } } + return copy; } private void processSingle(ObjectNode node, TsKvEntry entry) { @@ -216,7 +215,7 @@ public class TbGetTelemetryNode implements TbNode { if (limit != 0) { return limit; } else { - return MAX_FETCH_SIZE; + return TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java index cbbf00ce92..bcd54cd829 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java @@ -37,7 +37,8 @@ import org.thingsboard.server.common.data.util.TbPair; version = 1, nodeDescription = "Adds message originator tenant attributes or latest telemetry into message or message metadata", nodeDetails = "Useful when you need to retrieve some common configuration or threshold set " + - "that is stored as tenant attributes or telemetry data and use it for further message processing.", + "that is stored as tenant attributes or telemetry data and use it for further message processing.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeTenantAttributesConfig") public class TbGetTenantAttributeNode extends TbAbstractGetEntityDataNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java index 8e293aea37..54d95645e1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java @@ -23,6 +23,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -36,7 +37,8 @@ import org.thingsboard.server.common.msg.TbMsg; version = 1, nodeDescription = "Adds message originator tenant details into message or message metadata", nodeDetails = "Useful when we need to retrieve contact information from your tenant " + - "such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.", + "such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeEntityDetailsConfig") public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode { @@ -66,8 +68,8 @@ public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode(false, oldConfiguration); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeConfiguration.java index 35cdc42a54..f50f44ec23 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; import java.util.Collections; @@ -29,7 +30,7 @@ public class TbGetTenantDetailsNodeConfiguration extends TbAbstractGetEntityDeta public TbGetTenantDetailsNodeConfiguration defaultConfiguration() { var configuration = new TbGetTenantDetailsNodeConfiguration(); configuration.setDetailsList(Collections.emptyList()); - configuration.setFetchTo(FetchTo.DATA); + configuration.setFetchTo(TbMsgSource.DATA); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 121b9fb756..b9ee65675a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -25,7 +25,6 @@ import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttConnectResult; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; @@ -41,6 +40,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import javax.net.ssl.SSLException; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -58,7 +58,7 @@ import java.util.concurrent.TimeoutException; ) public class TbMqttNode extends TbAbstractExternalNode { - private static final Charset UTF8 = Charset.forName("UTF-8"); + private static final Charset UTF8 = StandardCharsets.UTF_8; private static final String ERROR = "error"; @@ -86,16 +86,16 @@ public class TbMqttNode extends TbAbstractExternalNode { if (future.isSuccess()) { tellSuccess(ctx, tbMsg); } else { - tellFailure(ctx, processException(ctx, tbMsg, future.cause()), future.cause()); + tellFailure(ctx, processException(tbMsg, future.cause()), future.cause()); } } ); } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) { + private TbMsg processException(TbMsg origMsg, Throwable e) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } @Override @@ -105,8 +105,13 @@ public class TbMqttNode extends TbAbstractExternalNode { } } + String getOwnerId(TbContext ctx) { + return "Tenant[" + ctx.getTenantId().getId() + "]RuleNode[" + ctx.getSelf().getId().getId() + "]"; + } + protected MqttClient initClient(TbContext ctx) throws Exception { MqttClientConfig config = new MqttClientConfig(getSslContext()); + config.setOwnerId(getOwnerId(ctx)); if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) { config.setClientId(this.mqttNodeConfiguration.isAppendClientIdSuffix() ? this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId()); @@ -114,7 +119,7 @@ public class TbMqttNode extends TbAbstractExternalNode { config.setCleanSession(this.mqttNodeConfiguration.isCleanSession()); prepareMqttClientConfig(config); - MqttClient client = MqttClient.create(config, null); + MqttClient client = MqttClient.create(config, null, ctx.getExternalCallExecutor()); client.setEventLoop(ctx.getSharedEventLoop()); Future connectFuture = client.connect(this.mqttNodeConfiguration.getHost(), this.mqttNodeConfiguration.getPort()); MqttConnectResult result; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java index f1fd9727fa..5f1bea2bdb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java @@ -19,7 +19,6 @@ import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; @@ -77,7 +76,7 @@ public class TbNotificationNode extends TbAbstractExternalNode { ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> { TbMsgMetaData metaData = tbMsg.getMetaData().copy(); metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats)); - tellSuccess(ctx, TbMsg.transformMsg(tbMsg, metaData)); + tellSuccess(ctx, TbMsg.transformMsgMetadata(tbMsg, metaData)); })), r -> { }, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index 70f15935fc..9e2719bf53 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -189,7 +190,7 @@ class AlarmState { metaData.putValue(DataConstants.IS_CLEARED_ALARM, Boolean.TRUE.toString()); } setAlarmConditionMetadata(ruleState, metaData); - TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : null, "ALARM", + TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : null, TbMsgType.ALARM, originator, msg != null ? msg.getCustomerId() : null, metaData, data); ctx.enqueueForTellNext(newMsg, relationType); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java index 31f661417b..2aeb4eedf5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java @@ -40,7 +40,6 @@ import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.dao.sql.query.EntityKeyMapping; @@ -55,6 +54,18 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_ACK; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_CLEAR; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_ASSIGNED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_UNASSIGNED; +import static org.thingsboard.server.common.data.msg.TbMsgType.INACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; + @Slf4j class DeviceState { @@ -136,24 +147,24 @@ class DeviceState { latestValues = fetchLatestValues(ctx, deviceId); } boolean stateChanged = false; - if (msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) { + if (msg.isTypeOf(POST_TELEMETRY_REQUEST)) { stateChanged = processTelemetry(ctx, msg); - } else if (msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) { + } else if (msg.isTypeOf(POST_ATTRIBUTES_REQUEST)) { stateChanged = processAttributesUpdateRequest(ctx, msg); - } else if (msg.getType().equals(DataConstants.ACTIVITY_EVENT) || msg.getType().equals(DataConstants.INACTIVITY_EVENT)) { + } else if (msg.isTypeOneOf(ACTIVITY_EVENT, INACTIVITY_EVENT)) { stateChanged = processDeviceActivityEvent(ctx, msg); - } else if (msg.getType().equals(DataConstants.ATTRIBUTES_UPDATED)) { + } else if (msg.isTypeOf(ATTRIBUTES_UPDATED)) { stateChanged = processAttributesUpdateNotification(ctx, msg); - } else if (msg.getType().equals(DataConstants.ATTRIBUTES_DELETED)) { + } else if (msg.isTypeOf(ATTRIBUTES_DELETED)) { stateChanged = processAttributesDeleteNotification(ctx, msg); - } else if (msg.getType().equals(DataConstants.ALARM_CLEAR)) { + } else if (msg.isTypeOf(ALARM_CLEAR)) { stateChanged = processAlarmClearNotification(ctx, msg); - } else if (msg.getType().equals(DataConstants.ALARM_ACK)) { + } else if (msg.isTypeOf(ALARM_ACK)) { processAlarmAckNotification(ctx, msg); - } else if (msg.getType().equals(DataConstants.ALARM_DELETE)) { + } else if (msg.isTypeOf(ALARM_DELETE)) { processAlarmDeleteNotification(ctx, msg); } else { - if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED) || msg.getType().equals(DataConstants.ENTITY_UNASSIGNED)) { + if (msg.isTypeOneOf(ENTITY_ASSIGNED, ENTITY_UNASSIGNED)) { dynamicPredicateValueCtx.resetCustomer(); } ctx.tellSuccess(msg); @@ -213,7 +224,7 @@ class DeviceState { private boolean processAttributesDeleteNotification(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { boolean stateChanged = false; List keys = new ArrayList<>(); - new JsonParser().parse(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray().forEach(e -> keys.add(e.getAsString())); + JsonParser.parseString(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray().forEach(e -> keys.add(e.getAsString())); String scope = msg.getMetaData().getValue(DataConstants.SCOPE); if (StringUtils.isEmpty(scope)) { scope = DataConstants.CLIENT_SCOPE; @@ -241,7 +252,7 @@ class DeviceState { private boolean processAttributes(TbContext ctx, TbMsg msg, String scope) throws ExecutionException, InterruptedException { boolean stateChanged = false; - Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())); + Set attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); if (!attributes.isEmpty()) { SnapshotUpdate update = merge(latestValues, attributes, scope); for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { @@ -256,7 +267,7 @@ class DeviceState { protected boolean processTelemetry(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { boolean stateChanged = false; - Map> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), msg.getMetaDataTs()); + Map> tsKvMap = JsonConverter.convertToSortedTelemetry(JsonParser.parseString(msg.getData()), msg.getMetaDataTs()); // iterate over data by ts (ASC order). for (Map.Entry> entry : tsKvMap.entrySet()) { Long ts = entry.getKey(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java index e2986677b5..5404b2ea0e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java @@ -26,12 +26,12 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -60,9 +60,6 @@ import java.util.concurrent.TimeUnit; configDirective = "tbDeviceProfileConfig" ) public class TbDeviceProfileNode implements TbNode { - private static final String PERIODIC_MSG_TYPE = "TbDeviceProfilePeriodicMsg"; - private static final String PROFILE_UPDATE_MSG_TYPE = "TbDeviceProfileUpdateMsg"; - private static final String DEVICE_UPDATE_MSG_TYPE = "TbDeviceUpdateMsg"; private TbDeviceProfileNodeConfiguration config; private RuleEngineDeviceProfileCache cache; @@ -111,12 +108,12 @@ public class TbDeviceProfileNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { EntityType originatorType = msg.getOriginator().getEntityType(); - if (msg.getType().equals(PERIODIC_MSG_TYPE)) { + if (msg.isTypeOf(TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG)) { scheduleAlarmHarvesting(ctx, msg); harvestAlarms(ctx, System.currentTimeMillis()); - } else if (msg.getType().equals(PROFILE_UPDATE_MSG_TYPE)) { + } else if (msg.isTypeOf(TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG)) { updateProfile(ctx, new DeviceProfileId(UUID.fromString(msg.getData()))); - } else if (msg.getType().equals(DEVICE_UPDATE_MSG_TYPE)) { + } else if (msg.isTypeOf(TbMsgType.DEVICE_UPDATE_SELF_MSG)) { JsonNode data = JacksonUtil.toJsonNode(msg.getData()); DeviceId deviceId = new DeviceId(UUID.fromString(data.get("deviceId").asText())); if (data.has("profileId")) { @@ -127,10 +124,10 @@ public class TbDeviceProfileNode implements TbNode { } else { if (EntityType.DEVICE.equals(originatorType)) { DeviceId deviceId = new DeviceId(msg.getOriginator().getId()); - if (msg.getType().equals(DataConstants.ENTITY_UPDATED)) { + if (msg.isTypeOf(TbMsgType.ENTITY_UPDATED)) { invalidateDeviceProfileCache(deviceId, msg.getData()); ctx.tellSuccess(msg); - } else if (msg.getType().equals(DataConstants.ENTITY_DELETED)) { + } else if (msg.isTypeOf(TbMsgType.ENTITY_DELETED)) { removeDeviceState(deviceId); ctx.tellSuccess(msg); } else { @@ -177,7 +174,7 @@ public class TbDeviceProfileNode implements TbNode { } protected void scheduleAlarmHarvesting(TbContext ctx, TbMsg msg) { - TbMsg periodicCheck = TbMsg.newMsg(PERIODIC_MSG_TYPE, ctx.getTenantId(), msg != null ? msg.getCustomerId() : null, TbMsgMetaData.EMPTY, "{}"); + TbMsg periodicCheck = TbMsg.newMsg(TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG, ctx.getTenantId(), msg != null ? msg.getCustomerId() : null, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); ctx.tellSelf(periodicCheck, TimeUnit.MINUTES.toMillis(1)); } @@ -202,7 +199,7 @@ public class TbDeviceProfileNode implements TbNode { } protected void onProfileUpdate(DeviceProfile profile) { - ctx.tellSelf(TbMsg.newMsg(PROFILE_UPDATE_MSG_TYPE, ctx.getTenantId(), TbMsgMetaData.EMPTY, profile.getId().getId().toString()), 0L); + ctx.tellSelf(TbMsg.newMsg(TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG, ctx.getTenantId(), TbMsgMetaData.EMPTY, profile.getId().getId().toString()), 0L); } private void onDeviceUpdate(DeviceId deviceId, DeviceProfile deviceProfile) { @@ -211,7 +208,7 @@ public class TbDeviceProfileNode implements TbNode { if (deviceProfile != null) { msgData.put("deviceProfileId", deviceProfile.getId().getId().toString()); } - ctx.tellSelf(TbMsg.newMsg(DEVICE_UPDATE_MSG_TYPE, ctx.getTenantId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(msgData)), 0L); + ctx.tellSelf(TbMsg.newMsg(TbMsgType.DEVICE_UPDATE_SELF_MSG, ctx.getTenantId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(msgData)), 0L); } protected void invalidateDeviceProfileCache(DeviceId deviceId, String deviceJson) { @@ -224,7 +221,7 @@ public class TbDeviceProfileNode implements TbNode { removeDeviceState(deviceId); } } catch (IllegalArgumentException e) { - log.debug("[{}] Received device update notification with non-device msg body: [{}][{}]", ctx.getSelfId(), deviceId, e); + log.debug("[{}] Received device update notification with non-device msg body: [{}]", ctx.getSelfId(), deviceId, e); } } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java index 4b42ee1d18..be7a22f713 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java @@ -24,7 +24,6 @@ import com.rabbitmq.client.MessageProperties; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; @@ -35,6 +34,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import static org.thingsboard.common.util.DonAsynchron.withCallback; @@ -51,7 +51,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; ) public class TbRabbitMqNode extends TbAbstractExternalNode { - private static final Charset UTF8 = Charset.forName("UTF-8"); + private static final Charset UTF8 = StandardCharsets.UTF_8; private static final String ERROR = "error"; @@ -87,7 +87,7 @@ public class TbRabbitMqNode extends TbAbstractExternalNode { var tbMsg = ackIfNeeded(ctx, msg); withCallback(publishMessageAsync(ctx, tbMsg), m -> tellSuccess(ctx, m), - t -> tellFailure(ctx, processException(ctx, tbMsg, t), t)); + t -> tellFailure(ctx, processException(tbMsg, t), t)); } private ListenableFuture publishMessageAsync(TbContext ctx, TbMsg msg) { @@ -115,10 +115,10 @@ public class TbRabbitMqNode extends TbAbstractExternalNode { return msg; } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) { + private TbMsg processException(TbMsg origMsg, Throwable t) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 28e221b0b1..0c9eca5f0a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -43,7 +43,6 @@ import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.credentials.BasicCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials; @@ -204,7 +203,7 @@ public class TbHttpClient { future.addCallback(new ListenableFutureCallback<>() { @Override public void onFailure(Throwable throwable) { - onFailure.accept(processException(ctx, msg, throwable), throwable); + onFailure.accept(processException(msg, throwable), throwable); } @Override @@ -212,7 +211,7 @@ public class TbHttpClient { if (responseEntity.getStatusCode().is2xxSuccessful()) { onSuccess.accept(processResponse(ctx, msg, responseEntity)); } else { - onFailure.accept(processFailureResponse(ctx, msg, responseEntity), null); + onFailure.accept(processFailureResponse(msg, responseEntity), null); } } }); @@ -261,8 +260,8 @@ public class TbHttpClient { metaData.putValue(STATUS_CODE, response.getStatusCode().value() + ""); metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase()); headersToMetaData(response.getHeaders(), metaData::putValue); - String body = response.getBody() == null ? "{}" : response.getBody(); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, body); + String body = response.getBody() == null ? TbMsg.EMPTY_JSON_OBJECT : response.getBody(); + return ctx.transformMsg(origMsg, metaData, body); } void headersToMetaData(Map> headers, BiConsumer consumer) { @@ -280,17 +279,17 @@ public class TbHttpClient { }); } - private TbMsg processFailureResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) { + private TbMsg processFailureResponse(TbMsg origMsg, ResponseEntity response) { TbMsgMetaData metaData = origMsg.getMetaData(); metaData.putValue(STATUS, response.getStatusCode().name()); metaData.putValue(STATUS_CODE, response.getStatusCode().value() + ""); metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase()); metaData.putValue(ERROR_BODY, response.getBody()); headersToMetaData(response.getHeaders(), metaData::putValue); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) { + private TbMsg processException(TbMsg origMsg, Throwable e) { TbMsgMetaData metaData = origMsg.getMetaData(); metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage()); if (e instanceof RestClientResponseException) { @@ -299,7 +298,7 @@ public class TbHttpClient { metaData.putValue(STATUS_CODE, restClientResponseException.getRawStatusCode() + ""); metaData.putValue(ERROR_BODY, restClientResponseException.getResponseBodyAsString()); } - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } private HttpHeaders prepareHeaders(TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index c6083f1098..281f65e1e4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.rest; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java index 79bd6f9bf9..4ae8d0e78e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java @@ -27,12 +27,13 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -56,7 +57,6 @@ public class TbSendRPCRequestNode implements TbNode { private Random random = new Random(); private Gson gson = new Gson(); - private JsonParser jsonParser = new JsonParser(); private TbSendRpcRequestNodeConfiguration config; @Override @@ -66,7 +66,7 @@ public class TbSendRPCRequestNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - JsonObject json = jsonParser.parse(msg.getData()).getAsJsonObject(); + JsonObject json = JsonParser.parseString(msg.getData()).getAsJsonObject(); String tmp; if (msg.getOriginator().getEntityType() != EntityType.DEVICE) { ctx.tellFailure(msg, new RuntimeException("Message originator is not a device entity!")); @@ -76,7 +76,7 @@ public class TbSendRPCRequestNode implements TbNode { ctx.tellFailure(msg, new RuntimeException("Params are not present in the message!")); } else { int requestId = json.has("requestId") ? json.get("requestId").getAsInt() : random.nextInt(); - boolean restApiCall = msg.getType().equals(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE); + boolean restApiCall = msg.isTypeOf(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE); tmp = msg.getMetaData().getValue("oneway"); boolean oneway = !StringUtils.isEmpty(tmp) && Boolean.parseBoolean(tmp); @@ -116,8 +116,8 @@ public class TbSendRPCRequestNode implements TbNode { ctx.getRpcService().sendRpcRequestToDevice(request, ruleEngineDeviceRpcResponse -> { if (ruleEngineDeviceRpcResponse.getError().isEmpty()) { - TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), ruleEngineDeviceRpcResponse.getResponse().orElse("{}")); - ctx.enqueueForTellNext(next, TbRelationTypes.SUCCESS); + TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), ruleEngineDeviceRpcResponse.getResponse().orElse(TbMsg.EMPTY_JSON_OBJECT)); + ctx.enqueueForTellNext(next, TbNodeConnectionType.SUCCESS); } else { TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name())); ctx.enqueueForTellFailure(next, ruleEngineDeviceRpcResponse.getError().get().name()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index 9cf9bfa1bd..0ae2a89abf 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -15,44 +15,58 @@ */ package org.thingsboard.rule.engine.telemetry; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.TbVersionedNode; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE; import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY; import static org.thingsboard.server.common.data.DataConstants.SCOPE; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; @Slf4j @RuleNode( type = ComponentType.ACTION, name = "save attributes", configClazz = TbMsgAttributesNodeConfiguration.class, + version = 1, nodeDescription = "Saves attributes data", nodeDetails = "Saves entity attributes based on configurable scope parameter. Expects messages with 'POST_ATTRIBUTES_REQUEST' message type. " + "If upsert(update/insert) operation is completed successfully rule node will send the incoming message via Success chain, otherwise, Failure chain is used. " + "Additionally if checkbox Send attributes updated notification is set to true, rule node will put the \"Attributes Updated\" " + - "event for SHARED_SCOPE and SERVER_SCOPE attributes updates to the corresponding rule engine queue.", + "event for SHARED_SCOPE and SERVER_SCOPE attributes updates to the corresponding rule engine queue." + + "Performance checkbox 'Save attributes only if the value changes' will skip attributes overwrites for values with no changes (avoid concurrent writes because this check is not transactional; will not update 'Last updated time' for skipped attributes).", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeAttributesConfig", icon = "file_upload" ) -public class TbMsgAttributesNode implements TbNode { +public class TbMsgAttributesNode implements TbVersionedNode { + static final String UPDATE_ATTRIBUTES_ONLY_ON_VALUE_CHANGE_KEY = "updateAttributesOnlyOnValueChange"; private TbMsgAttributesNodeConfiguration config; @Override @@ -65,18 +79,41 @@ public class TbMsgAttributesNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (!msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) { + if (!msg.isTypeOf(POST_ATTRIBUTES_REQUEST)) { ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); return; } String src = msg.getData(); - List attributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(src))); - if (attributes.isEmpty()) { + List newAttributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(src))); + if (newAttributes.isEmpty()) { ctx.tellSuccess(msg); return; } String scope = getScope(msg.getMetaData().getValue(SCOPE)); boolean sendAttributesUpdateNotification = checkSendNotification(scope); + + if (!config.isUpdateAttributesOnlyOnValueChange()) { + saveAttr(newAttributes, ctx, msg, scope, sendAttributesUpdateNotification); + return; + } + + List keys = newAttributes.stream().map(KvEntry::getKey).collect(Collectors.toList()); + ListenableFuture> findFuture = ctx.getAttributesService().find(ctx.getTenantId(), msg.getOriginator(), scope, keys); + + DonAsynchron.withCallback(findFuture, + currentAttributes -> { + List attributesChanged = filterChangedAttr(currentAttributes, newAttributes); + saveAttr(attributesChanged, ctx, msg, scope, sendAttributesUpdateNotification); + }, + throwable -> ctx.tellFailure(msg, throwable), + MoreExecutors.directExecutor()); + } + + void saveAttr(List attributes, TbContext ctx, TbMsg msg, String scope, boolean sendAttributesUpdateNotification) { + if (attributes.isEmpty()) { + ctx.tellSuccess(msg); + return; + } ctx.getTelemetryService().saveAndNotify( ctx.getTenantId(), msg.getOriginator(), @@ -89,6 +126,24 @@ public class TbMsgAttributesNode implements TbNode { ); } + List filterChangedAttr(List currentAttributes, List newAttributes) { + if (currentAttributes == null || currentAttributes.isEmpty()) { + return newAttributes; + } + + Map currentAttrMap = currentAttributes.stream() + .collect(Collectors.toMap(AttributeKvEntry::getKey, Function.identity(), (existing, replacement) -> existing)); + + return newAttributes.stream() + .filter(item -> { + AttributeKvEntry cacheAttr = currentAttrMap.get(item.getKey()); + return cacheAttr == null + || !Objects.equals(item.getValue(), cacheAttr.getValue()) //JSON and String can be equals by value, but different by type + || !Objects.equals(item.getDataType(), cacheAttr.getDataType()); + }) + .collect(Collectors.toList()); + } + private boolean checkSendNotification(String scope) { return config.isSendAttributesUpdatedNotification() && !CLIENT_SCOPE.equals(scope); } @@ -104,4 +159,20 @@ public class TbMsgAttributesNode implements TbNode { return config.getScope(); } + @Override + public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { + boolean hasChanges = false; + switch (fromVersion) { + case 0: + if (!oldConfiguration.has(UPDATE_ATTRIBUTES_ONLY_ON_VALUE_CHANGE_KEY)) { + hasChanges = true; + ((ObjectNode) oldConfiguration).put(UPDATE_ATTRIBUTES_ONLY_ON_VALUE_CHANGE_KEY, false); + } + break; + default: + break; + } + return new TbPair<>(hasChanges, oldConfiguration); + } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java index dd6140c4b5..1dd98feb16 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java @@ -26,6 +26,7 @@ public class TbMsgAttributesNodeConfiguration implements NodeConfiguration> tsKvMap = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts); + Map> tsKvMap = JsonConverter.convertToTelemetry(JsonParser.parseString(src), ts); if (tsKvMap.isEmpty()) { ctx.tellFailure(msg, new IllegalArgumentException("Msg body is empty: " + src)); return; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/MultipleTbMsgsCallbackWrapper.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/MultipleTbMsgsCallbackWrapper.java index ee63201fde..142aa8c085 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/MultipleTbMsgsCallbackWrapper.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/MultipleTbMsgsCallbackWrapper.java @@ -39,7 +39,7 @@ public class MultipleTbMsgsCallbackWrapper implements TbMsgCallbackWrapper { @Override public void onFailure(Throwable t) { - callback.onFailure(new RuleEngineException(t.getMessage())); + callback.onFailure(new RuleEngineException(t.getMessage(), t)); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java index 7bf08e8647..72570d4e2a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java @@ -22,7 +22,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -75,7 +75,7 @@ public abstract class TbAbstractTransformNode implements TbNode { ctx.tellFailure(msg, e); } }); - msgs.forEach(newMsg -> ctx.enqueueForTellNext(newMsg, TbRelationTypes.SUCCESS, wrapper::onSuccess, wrapper::onFailure)); + msgs.forEach(newMsg -> ctx.enqueueForTellNext(newMsg, TbNodeConnectionType.SUCCESS, wrapper::onSuccess, wrapper::onFailure)); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index 5cc5dfe23f..4d1a092097 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -34,7 +34,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; @@ -74,14 +73,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode metaDataMap.remove(key)); + keysToDelete.forEach(metaDataMap::remove); metaData = new TbMsgMetaData(metaDataMap); } else { JsonNode dataNode = JacksonUtil.toJsonNode(msgData); @@ -94,7 +94,7 @@ public class TbDeleteKeysNode implements TbNode { if (keysToDelete.isEmpty()) { ctx.tellSuccess(msg); } else { - ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), metaData, msgData)); + ctx.tellSuccess(TbMsg.transformMsg(msg, metaData, msgData)); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java index da7d765a95..0e85a50c34 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java @@ -70,7 +70,7 @@ public class TbJsonPathNode implements TbNode { if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(this.jsonPathValue)) { try { Object jsonPathData = jsonPath.read(msg.getData(), this.configurationJsonPath); - ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(jsonPathData))); + ctx.tellSuccess(TbMsg.transformMsgData(msg, JacksonUtil.toString(jsonPathData))); } catch (PathNotFoundException e) { ctx.tellFailure(msg, e); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java index 3fce7494ea..88e6dc1a8b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java @@ -74,7 +74,7 @@ public class TbRenameKeysNode implements TbNode { } metaData = new TbMsgMetaData(metaDataMap); } else { - JsonNode dataNode = JacksonUtil.toJsonNode(msg.getData()); + JsonNode dataNode = JacksonUtil.toJsonNode(data); if (dataNode.isObject()) { ObjectNode msgData = (ObjectNode) dataNode; for (Map.Entry entry : renameKeysMapping.entrySet()) { @@ -89,7 +89,7 @@ public class TbRenameKeysNode implements TbNode { } } if (msgChanged) { - ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), metaData, data)); + ctx.tellSuccess(TbMsg.transformMsg(msg, metaData, data)); } else { ctx.tellSuccess(msg); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java index ae7ceade97..8959d12c9f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java @@ -25,7 +25,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -64,7 +64,7 @@ public class TbSplitArrayMsgNode implements TbNode { if (data.isEmpty()) { ctx.ack(msg); } else if (data.size() == 1) { - ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(data.get(0)))); + ctx.tellSuccess(TbMsg.transformMsgData(msg, JacksonUtil.toString(data.get(0)))); } else { TbMsgCallbackWrapper wrapper = new MultipleTbMsgsCallbackWrapper(data.size(), new TbMsgCallback() { @Override @@ -78,8 +78,8 @@ public class TbSplitArrayMsgNode implements TbNode { } }); data.forEach(msgNode -> { - TbMsg outMsg = TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(msgNode)); - ctx.enqueueForTellNext(outMsg, TbRelationTypes.SUCCESS, wrapper::onSuccess, wrapper::onFailure); + TbMsg outMsg = TbMsg.transformMsgData(msg, JacksonUtil.toString(msgNode)); + ctx.enqueueForTellNext(outMsg, TbNodeConnectionType.SUCCESS, wrapper::onSuccess, wrapper::onFailure); }); } } else { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/FetchTo.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TbMsgSource.java similarity index 90% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/FetchTo.java rename to rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TbMsgSource.java index 811868bef0..e73b7976fa 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/FetchTo.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TbMsgSource.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.metadata; +package org.thingsboard.rule.engine.util; -public enum FetchTo { +public enum TbMsgSource { DATA, METADATA diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js index 2ad68f14b2..df584e6c9d 100644 --- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js +++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js @@ -1 +1 @@ -System.register(["@angular/core","@shared/public-api","@ngrx/store","@angular/forms","@angular/common","@angular/material/checkbox","@angular/material/input","@angular/material/form-field","@angular/flex-layout/flex","@ngx-translate/core","@angular/platform-browser","@angular/material/select","@angular/material/core","@shared/components/queue/queue-autocomplete.component","@core/public-api","@shared/components/js-func.component","@angular/material/button","@shared/components/script-lang.component","@angular/cdk/keycodes","@angular/material/icon","@angular/material/chips","@shared/components/entity/entity-type-select.component","@shared/components/entity/entity-select.component","@angular/cdk/coercion","@shared/components/tb-error.component","@angular/material/tooltip","@angular/flex-layout/extended","@angular/material/list","@angular/cdk/drag-drop","rxjs/operators","@angular/material/autocomplete","@shared/pipe/highlight.pipe","rxjs","@angular/material/expansion","@home/components/public-api","tslib","@shared/components/help-popup.component","@shared/components/entity/entity-subtype-list.component","@shared/components/relation/relation-type-autocomplete.component","@angular/material/slide-toggle","@home/components/relation/relation-filters.component","@shared/components/file-input.component","@shared/components/button/toggle-password.component","@shared/components/toggle-header.component","@shared/components/entity/entity-list.component","@shared/components/notification/template-autocomplete.component","@shared/components/tb-checkbox.component","@home/components/sms/sms-provider-configuration.component","@angular/material/radio","@shared/components/slack-conversation-autocomplete.component","@shared/components/entity/entity-autocomplete.component","@shared/components/entity/entity-type-list.component"],(function(e){"use strict";var t,n,r,o,a,i,l,s,m,u,p,d,c,f,g,y,x,b,h,C,v,F,L,k,T,I,N,S,q,M,A,G,E,D,V,w,P,R,O,H,K,B,U,z,_,j,$,Q,J,Y,W,X,Z,ee,te,ne,re,oe,ae,ie,le,se,me,ue,pe,de,ce,fe,ge,ye,xe,be,he,Ce,ve,Fe,Le,ke,Te,Ie,Ne,Se,qe,Me,Ae,Ge,Ee,De,Ve,we,Pe,Re,Oe,He,Ke,Be,Ue,ze,_e,je,$e,Qe;return{setters:[function(e){t=e,n=e.Component,r=e.Pipe,o=e.EventEmitter,a=e.ViewChild,i=e.forwardRef,l=e.Input,s=e.NgModule},function(e){m=e.RuleNodeConfigurationComponent,u=e.AttributeScope,p=e.telemetryTypeTranslations,d=e.ServiceType,c=e.ScriptLanguage,f=e.AlarmSeverity,g=e.alarmSeverityTranslations,y=e.EntitySearchDirection,x=e.entitySearchDirectionTranslations,b=e.EntityType,h=e.PageComponent,C=e.coerceBoolean,v=e.MessageType,F=e.messageTypeNames,L=e,k=e.SharedModule,T=e.AggregationType,I=e.aggregationTranslations,N=e.entityFields,S=e.NotificationType,q=e.SlackChanelType,M=e.SlackChanelTypesTranslateMap,A=e.alarmStatusTranslations,G=e.AlarmStatus},function(e){E=e},function(e){D=e,V=e.Validators,w=e.NgControl,P=e.NG_VALUE_ACCESSOR,R=e.NG_VALIDATORS,O=e.FormControl,H=e.UntypedFormControl},function(e){K=e,B=e.CommonModule},function(e){U=e},function(e){z=e},function(e){_=e},function(e){j=e},function(e){$=e},function(e){Q=e},function(e){J=e},function(e){Y=e},function(e){W=e},function(e){X=e.getCurrentAuthState,Z=e,ee=e.isDefinedAndNotNull,te=e.deepTrim,ne=e.isObject,re=e.isNotEmptyStr},function(e){oe=e},function(e){ae=e},function(e){ie=e},function(e){le=e.ENTER,se=e.COMMA,me=e.SEMICOLON},function(e){ue=e},function(e){pe=e},function(e){de=e},function(e){ce=e},function(e){fe=e.coerceBooleanProperty},function(e){ge=e},function(e){ye=e},function(e){xe=e},function(e){be=e},function(e){he=e},function(e){Ce=e.tap,ve=e.map,Fe=e.mergeMap,Le=e.takeUntil,ke=e.startWith,Te=e.share},function(e){Ie=e},function(e){Ne=e},function(e){Se=e.of,qe=e.Subject},function(e){Me=e},function(e){Ae=e.HomeComponentsModule},function(e){Ge=e.__decorate},function(e){Ee=e},function(e){De=e},function(e){Ve=e},function(e){we=e},function(e){Pe=e},function(e){Re=e},function(e){Oe=e},function(e){He=e},function(e){Ke=e},function(e){Be=e},function(e){Ue=e},function(e){ze=e},function(e){_e=e},function(e){je=e},function(e){$e=e},function(e){Qe=e}],execute:function(){class Je extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.emptyConfigForm}onConfigurationSet(e){this.emptyConfigForm=this.fb.group({})}}e("EmptyConfigComponent",Je),Je.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Je,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Je.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Je,selector:"tb-node-empty-config",usesInheritance:!0,ngImport:t,template:"
",isInline:!0}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Je,decorators:[{type:n,args:[{selector:"tb-node-empty-config",template:"
"}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Ye{constructor(e){this.sanitizer=e}transform(e){return this.sanitizer.bypassSecurityTrustHtml(e)}}e("SafeHtmlPipe",Ye),Ye.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ye,deps:[{token:Q.DomSanitizer}],target:t.ɵɵFactoryTarget.Pipe}),Ye.ɵpipe=t.ɵɵngDeclarePipe({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:Ye,name:"safeHtml"}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ye,decorators:[{type:r,args:[{name:"safeHtml"}]}],ctorParameters:function(){return[{type:Q.DomSanitizer}]}});class We extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.assignCustomerConfigForm}onConfigurationSet(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[V.required,V.pattern(/.*\S.*/)]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[V.required,V.min(0)]]})}prepareOutputConfig(e){return e.customerNamePattern=e.customerNamePattern.trim(),e}}e("AssignCustomerConfigComponent",We),We.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:We,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),We.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:We,selector:"tb-action-node-assign-to-customer-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:We,decorators:[{type:n,args:[{selector:"tb-action-node-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Xe extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopeMap=u,this.attributeScopes=Object.keys(u),this.telemetryTypeTranslationsMap=p}configForm(){return this.attributesConfigForm}onConfigurationSet(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[V.required]],notifyDevice:[!e||e.notifyDevice,[]],sendAttributesUpdatedNotification:[!!e&&e.sendAttributesUpdatedNotification,[]]}),this.attributesConfigForm.get("scope").valueChanges.subscribe((e=>{e!==u.SHARED_SCOPE&&this.attributesConfigForm.get("notifyDevice").patchValue(!1,{emitEvent:!1}),e===u.CLIENT_SCOPE&&this.attributesConfigForm.get("sendAttributesUpdatedNotification").patchValue(!1,{emitEvent:!1})}))}}e("AttributesConfigComponent",Xe),Xe.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Xe,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Xe.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Xe,selector:"tb-action-node-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-hint
\n
\n
\n \n {{ \'tb.rulenode.send-attributes-updated-notification\' | translate }}\n \n
tb.rulenode.send-attributes-updated-notification-hint
\n
\n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Xe,decorators:[{type:n,args:[{selector:"tb-action-node-attributes-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-hint
\n
\n
\n \n {{ \'tb.rulenode.send-attributes-updated-notification\' | translate }}\n \n
tb.rulenode.send-attributes-updated-notification-hint
\n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Ze extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.serviceType=d.TB_RULE_ENGINE}configForm(){return this.checkPointConfigForm}onConfigurationSet(e){this.checkPointConfigForm=this.fb.group({queueName:[e?e.queueName:null,[V.required]]})}}e("CheckPointConfigComponent",Ze),Ze.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ze,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ze.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Ze,selector:"tb-action-node-check-point-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n',dependencies:[{kind:"component",type:W.QueueAutocompleteComponent,selector:"tb-queue-autocomplete",inputs:["labelText","requiredText","autocompleteHint","subscriptSizing","required","queueType","disabled"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ze,decorators:[{type:n,args:[{selector:"tb-action-node-check-point-config",template:'
\n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class et extends m{constructor(e,t,n,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=r,this.tbelEnabled=X(this.store).tbelEnabled,this.scriptLanguage=c,this.changeScript=new o,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-details-function"}configForm(){return this.clearAlarmConfigForm}onConfigurationSet(e){this.clearAlarmConfigForm=this.fb.group({scriptLang:[e?e.scriptLang:c.JS,[V.required]],alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[]],alarmDetailsBuildTbel:[e?e.alarmDetailsBuildTbel:null,[]],alarmType:[e?e.alarmType:null,[V.required]]})}validatorTriggers(){return["scriptLang"]}updateValidators(e){let t=this.clearAlarmConfigForm.get("scriptLang").value;t!==c.TBEL||this.tbelEnabled||(t=c.JS,this.clearAlarmConfigForm.get("scriptLang").patchValue(t,{emitEvent:!1}),setTimeout((()=>{this.clearAlarmConfigForm.updateValueAndValidity({emitEvent:!0})}))),this.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValidators(t===c.JS?[V.required]:[]),this.clearAlarmConfigForm.get("alarmDetailsBuildJs").updateValueAndValidity({emitEvent:e}),this.clearAlarmConfigForm.get("alarmDetailsBuildTbel").setValidators(t===c.TBEL?[V.required]:[]),this.clearAlarmConfigForm.get("alarmDetailsBuildTbel").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=c.JS)),e}testScript(e){const t=this.clearAlarmConfigForm.get("scriptLang").value,n=t===c.JS?"alarmDetailsBuildJs":"alarmDetailsBuildTbel",r=t===c.JS?"rulenode/clear_alarm_node_script_fn":"rulenode/tbel/clear_alarm_node_script_fn",o=this.clearAlarmConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.clearAlarmConfigForm.get(n).setValue(e),this.changeScript.emit())}))}onValidate(){this.clearAlarmConfigForm.get("scriptLang").value===c.JS&&this.jsFuncComponent.validateOnSubmit()}}e("ClearAlarmConfigComponent",et),et.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:et,deps:[{token:E.Store},{token:D.UntypedFormBuilder},{token:Z.NodeScriptTestService},{token:$.TranslateService}],target:t.ɵɵFactoryTarget.Component}),et.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:et,selector:"tb-action-node-clear-alarm-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:oe.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:ae.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:ie.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:et,decorators:[{type:n,args:[{selector:"tb-action-node-clear-alarm-config",template:'
\n \n \n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder},{type:Z.NodeScriptTestService},{type:$.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:a,args:["tbelFuncComponent",{static:!1}]}]}});class tt extends m{constructor(e,t,n,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=r,this.alarmSeverities=Object.keys(f),this.alarmSeverityTranslationMap=g,this.separatorKeysCodes=[le,se,me],this.tbelEnabled=X(this.store).tbelEnabled,this.scriptLanguage=c,this.changeScript=new o,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-details-function"}configForm(){return this.createAlarmConfigForm}onConfigurationSet(e){this.createAlarmConfigForm=this.fb.group({scriptLang:[e?e.scriptLang:c.JS,[V.required]],alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[]],alarmDetailsBuildTbel:[e?e.alarmDetailsBuildTbel:null,[]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],overwriteAlarmDetails:[!!e&&e.overwriteAlarmDetails,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]],propagateToOwner:[!!e&&e.propagateToOwner,[]],propagateToTenant:[!!e&&e.propagateToTenant,[]],dynamicSeverity:!1}),this.createAlarmConfigForm.get("dynamicSeverity").valueChanges.subscribe((e=>{e?this.createAlarmConfigForm.get("severity").patchValue("",{emitEvent:!1}):this.createAlarmConfigForm.get("severity").patchValue(this.alarmSeverities[0],{emitEvent:!1})}))}validatorTriggers(){return["useMessageAlarmData","overwriteAlarmDetails","scriptLang"]}updateValidators(e){const t=this.createAlarmConfigForm.get("useMessageAlarmData").value,n=this.createAlarmConfigForm.get("overwriteAlarmDetails").value;t?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([V.required]),this.createAlarmConfigForm.get("severity").setValidators([V.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e});let r=this.createAlarmConfigForm.get("scriptLang").value;r!==c.TBEL||this.tbelEnabled||(r=c.JS,this.createAlarmConfigForm.get("scriptLang").patchValue(r,{emitEvent:!1}),setTimeout((()=>{this.createAlarmConfigForm.updateValueAndValidity({emitEvent:!0})})));const o=!1===t||!0===n;this.createAlarmConfigForm.get("alarmDetailsBuildJs").setValidators(o&&r===c.JS?[V.required]:[]),this.createAlarmConfigForm.get("alarmDetailsBuildTbel").setValidators(o&&r===c.TBEL?[V.required]:[]),this.createAlarmConfigForm.get("alarmDetailsBuildJs").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("alarmDetailsBuildTbel").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=c.JS)),e}testScript(e){const t=this.createAlarmConfigForm.get("scriptLang").value,n=t===c.JS?"alarmDetailsBuildJs":"alarmDetailsBuildTbel",r=t===c.JS?"rulenode/create_alarm_node_script_fn":"rulenode/tbel/create_alarm_node_script_fn",o=this.createAlarmConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.createAlarmConfigForm.get(n).setValue(e),this.changeScript.emit())}))}removeKey(e,t){const n=this.createAlarmConfigForm.get(t).value,r=n.indexOf(e);r>=0&&(n.splice(r,1),this.createAlarmConfigForm.get(t).setValue(n,{emitEvent:!0}))}addKey(e,t){const n=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.createAlarmConfigForm.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.createAlarmConfigForm.get(t).setValue(e,{emitEvent:!0}))}n&&(n.value="")}onValidate(){const e=this.createAlarmConfigForm.get("useMessageAlarmData").value,t=this.createAlarmConfigForm.get("overwriteAlarmDetails").value;if(!e||t){this.createAlarmConfigForm.get("scriptLang").value===c.JS&&this.jsFuncComponent.validateOnSubmit()}}}e("CreateAlarmConfigComponent",tt),tt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:tt,deps:[{token:E.Store},{token:D.UntypedFormBuilder},{token:Z.NodeScriptTestService},{token:$.TranslateService}],target:t.ɵɵFactoryTarget.Component}),tt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:tt,selector:"tb-action-node-create-alarm-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n \n {{ \'tb.rulenode.overwrite-alarm-details\' | translate }}\n \n
\n \n \n \n \n \n
\n \n
\n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-alarm-severity-pattern\' | translate }}\n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n tb.rulenode.alarm-severity-pattern\n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n tb.rulenode.relation-types-list-hint\n \n
\n \n {{ \'tb.rulenode.propagate-to-owner\' | translate }}\n \n \n {{ \'tb.rulenode.propagate-to-tenant\' | translate }}\n \n
\n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:oe.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:ae.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:pe.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:pe.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:pe.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:pe.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:ie.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:tt,decorators:[{type:n,args:[{selector:"tb-action-node-create-alarm-config",template:'
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n \n {{ \'tb.rulenode.overwrite-alarm-details\' | translate }}\n \n
\n \n \n \n \n \n
\n \n
\n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-alarm-severity-pattern\' | translate }}\n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n tb.rulenode.alarm-severity-pattern\n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n tb.rulenode.relation-types-list-hint\n \n
\n \n {{ \'tb.rulenode.propagate-to-owner\' | translate }}\n \n \n {{ \'tb.rulenode.propagate-to-tenant\' | translate }}\n \n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder},{type:Z.NodeScriptTestService},{type:$.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:a,args:["tbelFuncComponent",{static:!1}]}]}});class nt extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(y),this.directionTypeTranslations=x,this.entityType=b}configForm(){return this.createRelationConfigForm}onConfigurationSet(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[V.required]],entityType:[e?e.entityType:null,[V.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[V.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[V.required,V.min(0)]]})}validatorTriggers(){return["entityType"]}updateValidators(e){const t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([V.required,V.pattern(/.*\S.*/)]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==b.DEVICE&&t!==b.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([V.required,V.pattern(/.*\S.*/)]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})}prepareOutputConfig(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e.entityTypePattern=e.entityTypePattern?e.entityTypePattern.trim():null,e}}e("CreateRelationConfigComponent",nt),nt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:nt,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),nt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:nt,selector:"tb-action-node-create-relation-config",usesInheritance:!0,ngImport:t,template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:de.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","filterAllowedEntityTypes","showLabel","required","disabled"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:nt,decorators:[{type:n,args:[{selector:"tb-action-node-create-relation-config",template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class rt extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(y),this.directionTypeTranslations=x,this.entityType=b}configForm(){return this.deleteRelationConfigForm}onConfigurationSet(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[V.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[V.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[V.required,V.min(0)]]})}validatorTriggers(){return["deleteForSingleEntity","entityType"]}updateValidators(e){const t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,n=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([V.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&n?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([V.required,V.pattern(/.*\S.*/)]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})}prepareOutputConfig(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e}}e("DeleteRelationConfigComponent",rt),rt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:rt,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),rt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:rt,selector:"tb-action-node-delete-relation-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:de.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","filterAllowedEntityTypes","showLabel","required","disabled"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:rt,decorators:[{type:n,args:[{selector:"tb-action-node-delete-relation-config",template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class ot extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.deviceProfile}onConfigurationSet(e){this.deviceProfile=this.fb.group({persistAlarmRulesState:[!!e&&e.persistAlarmRulesState,V.required],fetchAlarmRulesStateOnStart:[!!e&&e.fetchAlarmRulesStateOnStart,V.required]})}}e("DeviceProfileConfigComponent",ot),ot.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ot,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),ot.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:ot,selector:"tb-device-profile-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.persist-alarm-rules\' | translate }}\n \n \n {{ \'tb.rulenode.fetch-alarm-rules\' | translate }}\n \n
\n',dependencies:[{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ot,decorators:[{type:n,args:[{selector:"tb-device-profile-config",template:'
\n \n {{ \'tb.rulenode.persist-alarm-rules\' | translate }}\n \n \n {{ \'tb.rulenode.fetch-alarm-rules\' | translate }}\n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class at extends m{constructor(e,t,n,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=r,this.tbelEnabled=X(this.store).tbelEnabled,this.scriptLanguage=c,this.changeScript=new o,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-generator-function",this.serviceType=d.TB_RULE_ENGINE}configForm(){return this.generatorConfigForm}onConfigurationSet(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[V.required,V.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[V.required,V.min(1)]],originator:[e?e.originator:null,[]],scriptLang:[e?e.scriptLang:c.JS,[V.required]],jsScript:[e?e.jsScript:null,[]],tbelScript:[e?e.tbelScript:null,[]],queueName:[e?e.queueName:null,[]]})}validatorTriggers(){return["scriptLang"]}updateValidators(e){let t=this.generatorConfigForm.get("scriptLang").value;t!==c.TBEL||this.tbelEnabled||(t=c.JS,this.generatorConfigForm.get("scriptLang").patchValue(t,{emitEvent:!1}),setTimeout((()=>{this.generatorConfigForm.updateValueAndValidity({emitEvent:!0})}))),this.generatorConfigForm.get("jsScript").setValidators(t===c.JS?[V.required]:[]),this.generatorConfigForm.get("jsScript").updateValueAndValidity({emitEvent:e}),this.generatorConfigForm.get("tbelScript").setValidators(t===c.TBEL?[V.required]:[]),this.generatorConfigForm.get("tbelScript").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=c.JS),e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e}prepareOutputConfig(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e}testScript(e){const t=this.generatorConfigForm.get("scriptLang").value,n=t===c.JS?"jsScript":"tbelScript",r=t===c.JS?"rulenode/generator_node_script_fn":"rulenode/tbel/generator_node_script_fn",o=this.generatorConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.generatorConfigForm.get(n).setValue(e),this.changeScript.emit())}))}onValidate(){this.generatorConfigForm.get("scriptLang").value===c.JS&&this.jsFuncComponent.validateOnSubmit()}}var it;e("GeneratorConfigComponent",at),at.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:at,deps:[{token:E.Store},{token:D.UntypedFormBuilder},{token:Z.NodeScriptTestService},{token:$.TranslateService}],target:t.ɵɵFactoryTarget.Component}),at.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:at,selector:"tb-action-node-generator-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n\n \n \n\n \n \n \n \n \n
\n \n
\n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ce.EntitySelectComponent,selector:"tb-entity-select",inputs:["allowedEntityTypes","useAliasEntityTypes","required","disabled"]},{kind:"component",type:W.QueueAutocompleteComponent,selector:"tb-queue-autocomplete",inputs:["labelText","requiredText","autocompleteHint","subscriptSizing","required","queueType","disabled"]},{kind:"component",type:oe.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:ae.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:ie.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:at,decorators:[{type:n,args:[{selector:"tb-action-node-generator-config",template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n\n \n \n\n \n \n \n \n \n
\n \n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder},{type:Z.NodeScriptTestService},{type:$.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:a,args:["tbelFuncComponent",{static:!1}]}]}}),function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR",e.ENTITY="ENTITY"}(it||(it={}));const lt=new Map([[it.CUSTOMER,"tb.rulenode.originator-customer"],[it.TENANT,"tb.rulenode.originator-tenant"],[it.RELATED,"tb.rulenode.originator-related"],[it.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"],[it.ENTITY,"tb.rulenode.originator-entity"]]);var st;!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(st||(st={}));const mt=new Map([[st.CIRCLE,"tb.rulenode.perimeter-circle"],[st.POLYGON,"tb.rulenode.perimeter-polygon"]]);var ut;!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(ut||(ut={}));const pt=new Map([[ut.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[ut.SECONDS,"tb.rulenode.time-unit-seconds"],[ut.MINUTES,"tb.rulenode.time-unit-minutes"],[ut.HOURS,"tb.rulenode.time-unit-hours"],[ut.DAYS,"tb.rulenode.time-unit-days"]]);var dt;!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(dt||(dt={}));const ct=new Map([[dt.METER,"tb.rulenode.range-unit-meter"],[dt.KILOMETER,"tb.rulenode.range-unit-kilometer"],[dt.FOOT,"tb.rulenode.range-unit-foot"],[dt.MILE,"tb.rulenode.range-unit-mile"],[dt.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);var ft;!function(e){e.ID="ID",e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.CITY="CITY",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(ft||(ft={}));const gt=new Map([[ft.ID,"tb.rulenode.entity-details-id"],[ft.TITLE,"tb.rulenode.entity-details-title"],[ft.COUNTRY,"tb.rulenode.entity-details-country"],[ft.STATE,"tb.rulenode.entity-details-state"],[ft.CITY,"tb.rulenode.entity-details-city"],[ft.ZIP,"tb.rulenode.entity-details-zip"],[ft.ADDRESS,"tb.rulenode.entity-details-address"],[ft.ADDRESS2,"tb.rulenode.entity-details-address2"],[ft.PHONE,"tb.rulenode.entity-details-phone"],[ft.EMAIL,"tb.rulenode.entity-details-email"],[ft.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);var yt;!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(yt||(yt={}));const xt=new Map([[yt.FIRST,"tb.rulenode.first"],[yt.LAST,"tb.rulenode.last"],[yt.ALL,"tb.rulenode.all"]]),bt=new Map([[yt.FIRST,"tb.rulenode.first-mode-hint"],[yt.LAST,"tb.rulenode.last-mode-hint"],[yt.ALL,"tb.rulenode.all-mode-hint"]]);var ht,Ct;!function(e){e.ASC="ASC",e.DESC="DESC"}(ht||(ht={})),function(e){e.ATTRIBUTES="ATTRIBUTES",e.LATEST_TELEMETRY="LATEST_TELEMETRY",e.FIELDS="FIELDS"}(Ct||(Ct={}));const vt=new Map([[Ct.ATTRIBUTES,"tb.rulenode.attributes"],[Ct.LATEST_TELEMETRY,"tb.rulenode.latest-telemetry"],[Ct.FIELDS,"tb.rulenode.fields"]]),Ft=new Map([[Ct.ATTRIBUTES,"tb.rulenode.add-mapped-attribute-to"],[Ct.LATEST_TELEMETRY,"tb.rulenode.add-mapped-latest-telemetry-to"],[Ct.FIELDS,"tb.rulenode.add-mapped-fields-to"]]),Lt=new Map([[ht.ASC,"tb.rulenode.ascending"],[ht.DESC,"tb.rulenode.descending"]]);var kt;!function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(kt||(kt={}));const Tt=new Map([[kt.STANDARD,"tb.rulenode.sqs-queue-standard"],[kt.FIFO,"tb.rulenode.sqs-queue-fifo"]]),It=["anonymous","basic","cert.PEM"],Nt=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),St=["sas","cert.PEM"],qt=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);var Mt;!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(Mt||(Mt={}));const At=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],Gt=new Map([["US-ASCII","tb.rulenode.charset-us-ascii"],["ISO-8859-1","tb.rulenode.charset-iso-8859-1"],["UTF-8","tb.rulenode.charset-utf-8"],["UTF-16BE","tb.rulenode.charset-utf-16be"],["UTF-16LE","tb.rulenode.charset-utf-16le"],["UTF-16","tb.rulenode.charset-utf-16"]]);var Et;!function(e){e.CUSTOM="CUSTOM",e.ADD="ADD",e.SUB="SUB",e.MULT="MULT",e.DIV="DIV",e.SIN="SIN",e.SINH="SINH",e.COS="COS",e.COSH="COSH",e.TAN="TAN",e.TANH="TANH",e.ACOS="ACOS",e.ASIN="ASIN",e.ATAN="ATAN",e.ATAN2="ATAN2",e.EXP="EXP",e.EXPM1="EXPM1",e.SQRT="SQRT",e.CBRT="CBRT",e.GET_EXP="GET_EXP",e.HYPOT="HYPOT",e.LOG="LOG",e.LOG10="LOG10",e.LOG1P="LOG1P",e.CEIL="CEIL",e.FLOOR="FLOOR",e.FLOOR_DIV="FLOOR_DIV",e.FLOOR_MOD="FLOOR_MOD",e.ABS="ABS",e.MIN="MIN",e.MAX="MAX",e.POW="POW",e.SIGNUM="SIGNUM",e.RAD="RAD",e.DEG="DEG"}(Et||(Et={}));const Dt=new Map([[Et.CUSTOM,{value:Et.CUSTOM,name:"Custom Function",description:"Use this function to specify complex mathematical expression.",minArgs:1,maxArgs:16}],[Et.ADD,{value:Et.ADD,name:"Addition",description:"x + y",minArgs:2,maxArgs:2}],[Et.SUB,{value:Et.SUB,name:"Subtraction",description:"x - y",minArgs:2,maxArgs:2}],[Et.MULT,{value:Et.MULT,name:"Multiplication",description:"x * y",minArgs:2,maxArgs:2}],[Et.DIV,{value:Et.DIV,name:"Division",description:"x / y",minArgs:2,maxArgs:2}],[Et.SIN,{value:Et.SIN,name:"Sine",description:"Returns the trigonometric sine of an angle in radians.",minArgs:1,maxArgs:1}],[Et.SINH,{value:Et.SINH,name:"Hyperbolic sine",description:"Returns the hyperbolic sine of an argument.",minArgs:1,maxArgs:1}],[Et.COS,{value:Et.COS,name:"Cosine",description:"Returns the trigonometric cosine of an angle in radians.",minArgs:1,maxArgs:1}],[Et.COSH,{value:Et.COSH,name:"Hyperbolic cosine",description:"Returns the hyperbolic cosine of an argument.",minArgs:1,maxArgs:1}],[Et.TAN,{value:Et.TAN,name:"Tangent",description:"Returns the trigonometric tangent of an angle in radians",minArgs:1,maxArgs:1}],[Et.TANH,{value:Et.TANH,name:"Hyperbolic tangent",description:"Returns the hyperbolic tangent of an argument",minArgs:1,maxArgs:1}],[Et.ACOS,{value:Et.ACOS,name:"Arc cosine",description:"Returns the arc cosine of an argument",minArgs:1,maxArgs:1}],[Et.ASIN,{value:Et.ASIN,name:"Arc sine",description:"Returns the arc sine of an argument",minArgs:1,maxArgs:1}],[Et.ATAN,{value:Et.ATAN,name:"Arc tangent",description:"Returns the arc tangent of an argument",minArgs:1,maxArgs:1}],[Et.ATAN2,{value:Et.ATAN2,name:"2-argument arc tangent",description:"Returns the angle theta from the conversion of rectangular coordinates (x, y) to polar coordinates (r, theta)",minArgs:2,maxArgs:2}],[Et.EXP,{value:Et.EXP,name:"Exponential",description:"Returns Euler's number e raised to the power of an argument",minArgs:1,maxArgs:1}],[Et.EXPM1,{value:Et.EXPM1,name:"Exponential minus one",description:"Returns Euler's number e raised to the power of an argument minus one",minArgs:1,maxArgs:1}],[Et.SQRT,{value:Et.SQRT,name:"Square",description:"Returns the correctly rounded positive square root of an argument",minArgs:1,maxArgs:1}],[Et.CBRT,{value:Et.CBRT,name:"Cube root",description:"Returns the cube root of an argument",minArgs:1,maxArgs:1}],[Et.GET_EXP,{value:Et.GET_EXP,name:"Get exponent",description:"Returns the unbiased exponent used in the representation of an argument",minArgs:1,maxArgs:1}],[Et.HYPOT,{value:Et.HYPOT,name:"Square root",description:"Returns the square root of the squares of the arguments",minArgs:2,maxArgs:2}],[Et.LOG,{value:Et.LOG,name:"Logarithm",description:"Returns the natural logarithm of an argument",minArgs:1,maxArgs:1}],[Et.LOG10,{value:Et.LOG10,name:"Base 10 logarithm",description:"Returns the base 10 logarithm of an argument",minArgs:1,maxArgs:1}],[Et.LOG1P,{value:Et.LOG1P,name:"Logarithm of the sum",description:"Returns the natural logarithm of the sum of an argument",minArgs:1,maxArgs:1}],[Et.CEIL,{value:Et.CEIL,name:"Ceiling",description:"Returns the smallest (closest to negative infinity) of an argument",minArgs:1,maxArgs:1}],[Et.FLOOR,{value:Et.FLOOR,name:"Floor",description:"Returns the largest (closest to positive infinity) of an argument",minArgs:1,maxArgs:1}],[Et.FLOOR_DIV,{value:Et.FLOOR_DIV,name:"Floor division",description:"Returns the largest (closest to positive infinity) of the arguments",minArgs:2,maxArgs:2}],[Et.FLOOR_MOD,{value:Et.FLOOR_MOD,name:"Floor modulus",description:"Returns the floor modulus of the arguments",minArgs:2,maxArgs:2}],[Et.ABS,{value:Et.ABS,name:"Absolute",description:"Returns the absolute value of an argument",minArgs:1,maxArgs:1}],[Et.MIN,{value:Et.MIN,name:"Min",description:"Returns the smaller of the arguments",minArgs:2,maxArgs:2}],[Et.MAX,{value:Et.MAX,name:"Max",description:"Returns the greater of the arguments",minArgs:2,maxArgs:2}],[Et.POW,{value:Et.POW,name:"Raise to a power",description:"Returns the value of the first argument raised to the power of the second argument",minArgs:2,maxArgs:2}],[Et.SIGNUM,{value:Et.SIGNUM,name:"Sign of a real number",description:"Returns the signum function of the argument",minArgs:1,maxArgs:1}],[Et.RAD,{value:Et.RAD,name:"Radian",description:"Converts an angle measured in degrees to an approximately equivalent angle measured in radians",minArgs:1,maxArgs:1}],[Et.DEG,{value:Et.DEG,name:"Degrees",description:"Converts an angle measured in radians to an approximately equivalent angle measured in degrees.",minArgs:1,maxArgs:1}]]);var Vt,wt,Pt;!function(e){e.ATTRIBUTE="ATTRIBUTE",e.TIME_SERIES="TIME_SERIES",e.CONSTANT="CONSTANT",e.MESSAGE_BODY="MESSAGE_BODY",e.MESSAGE_METADATA="MESSAGE_METADATA"}(Vt||(Vt={})),function(e){e.ATTRIBUTE="ATTRIBUTE",e.TIME_SERIES="TIME_SERIES",e.MESSAGE_BODY="MESSAGE_BODY",e.MESSAGE_METADATA="MESSAGE_METADATA"}(wt||(wt={})),function(e){e.DATA="DATA",e.METADATA="METADATA"}(Pt||(Pt={}));const Rt=new Map([[Pt.DATA,"tb.rulenode.message"],[Pt.METADATA,"tb.rulenode.metadata"]]),Ot=new Map([[Vt.ATTRIBUTE,"tb.rulenode.attribute-type"],[Vt.TIME_SERIES,"tb.rulenode.time-series-type"],[Vt.CONSTANT,"tb.rulenode.constant-type"],[Vt.MESSAGE_BODY,"tb.rulenode.message-body-type"],[Vt.MESSAGE_METADATA,"tb.rulenode.message-metadata-type"]]),Ht=["x","y","z","a","b","c","d","k","l","m","n","o","p","r","s","t"];var Kt,Bt;!function(e){e.SHARED_SCOPE="SHARED_SCOPE",e.SERVER_SCOPE="SERVER_SCOPE",e.CLIENT_SCOPE="CLIENT_SCOPE"}(Kt||(Kt={})),function(e){e.SHARED_SCOPE="SHARED_SCOPE",e.SERVER_SCOPE="SERVER_SCOPE"}(Bt||(Bt={}));const Ut=new Map([[Kt.SHARED_SCOPE,"tb.rulenode.shared-scope"],[Kt.SERVER_SCOPE,"tb.rulenode.server-scope"],[Kt.CLIENT_SCOPE,"tb.rulenode.client-scope"]]);class zt extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.perimeterType=st,this.perimeterTypes=Object.keys(st),this.perimeterTypeTranslationMap=mt,this.rangeUnits=Object.keys(dt),this.rangeUnitTranslationMap=ct,this.timeUnits=Object.keys(ut),this.timeUnitsTranslationMap=pt}configForm(){return this.geoActionConfigForm}onConfigurationSet(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[V.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[V.required]],perimeterType:[e?e.perimeterType:null,[V.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterKeyName:[e?e.perimeterKeyName:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]],minInsideDuration:[e?e.minInsideDuration:null,[V.required,V.min(1),V.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[V.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[V.required,V.min(1),V.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[V.required]]})}validatorTriggers(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]}updateValidators(e){const t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,n=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterKeyName").setValidators([V.required]):this.geoActionConfigForm.get("perimeterKeyName").setValidators([]),t||n!==st.CIRCLE?(this.geoActionConfigForm.get("centerLatitude").setValidators([]),this.geoActionConfigForm.get("centerLongitude").setValidators([]),this.geoActionConfigForm.get("range").setValidators([]),this.geoActionConfigForm.get("rangeUnit").setValidators([])):(this.geoActionConfigForm.get("centerLatitude").setValidators([V.required,V.min(-90),V.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([V.required,V.min(-180),V.max(180)]),this.geoActionConfigForm.get("range").setValidators([V.required,V.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([V.required])),t||n!==st.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([V.required]),this.geoActionConfigForm.get("perimeterKeyName").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})}}e("GpsGeoActionConfigComponent",zt),zt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:zt,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),zt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:zt,selector:"tb-action-node-gps-geofencing-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n \n tb.rulenode.perimeter-key-name\n \n \n {{ \'tb.rulenode.perimeter-key-name-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:zt,decorators:[{type:n,args:[{selector:"tb-action-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n \n tb.rulenode.perimeter-key-name\n \n \n {{ \'tb.rulenode.perimeter-key-name-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class _t extends m{constructor(e,t,n,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=r,this.tbelEnabled=X(this.store).tbelEnabled,this.scriptLanguage=c,this.changeScript=new o,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-to-string-function"}configForm(){return this.logConfigForm}onConfigurationSet(e){this.logConfigForm=this.fb.group({scriptLang:[e?e.scriptLang:c.JS,[V.required]],jsScript:[e?e.jsScript:null,[]],tbelScript:[e?e.tbelScript:null,[]]})}validatorTriggers(){return["scriptLang"]}updateValidators(e){let t=this.logConfigForm.get("scriptLang").value;t!==c.TBEL||this.tbelEnabled||(t=c.JS,this.logConfigForm.get("scriptLang").patchValue(t,{emitEvent:!1}),setTimeout((()=>{this.logConfigForm.updateValueAndValidity({emitEvent:!0})}))),this.logConfigForm.get("jsScript").setValidators(t===c.JS?[V.required]:[]),this.logConfigForm.get("jsScript").updateValueAndValidity({emitEvent:e}),this.logConfigForm.get("tbelScript").setValidators(t===c.TBEL?[V.required]:[]),this.logConfigForm.get("tbelScript").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=c.JS)),e}testScript(e){const t=this.logConfigForm.get("scriptLang").value,n=t===c.JS?"jsScript":"tbelScript",r=t===c.JS?"rulenode/log_node_script_fn":"rulenode/tbel/log_node_script_fn",o=this.logConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.logConfigForm.get(n).setValue(e),this.changeScript.emit())}))}onValidate(){this.logConfigForm.get("scriptLang").value===c.JS&&this.jsFuncComponent.validateOnSubmit()}}e("LogConfigComponent",_t),_t.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:_t,deps:[{token:E.Store},{token:D.UntypedFormBuilder},{token:Z.NodeScriptTestService},{token:$.TranslateService}],target:t.ɵɵFactoryTarget.Component}),_t.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:_t,selector:"tb-action-node-log-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n
\n \n
\n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:oe.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:ae.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:ie.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:_t,decorators:[{type:n,args:[{selector:"tb-action-node-log-config",template:'
\n \n \n \n \n \n
\n \n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder},{type:Z.NodeScriptTestService},{type:$.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:a,args:["tbelFuncComponent",{static:!1}]}]}});class jt extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.msgCountConfigForm}onConfigurationSet(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[V.required,V.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[V.required]]})}}e("MsgCountConfigComponent",jt),jt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:jt,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),jt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:jt,selector:"tb-action-node-msg-count-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:jt,decorators:[{type:n,args:[{selector:"tb-action-node-msg-count-config",template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class $t extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.msgDelayConfigForm}onConfigurationSet(e){this.msgDelayConfigForm=this.fb.group({useMetadataPeriodInSecondsPatterns:[!!e&&e.useMetadataPeriodInSecondsPatterns,[]],periodInSeconds:[e?e.periodInSeconds:null,[]],periodInSecondsPattern:[e?e.periodInSecondsPattern:null,[]],maxPendingMsgs:[e?e.maxPendingMsgs:null,[V.required,V.min(1),V.max(1e5)]]})}validatorTriggers(){return["useMetadataPeriodInSecondsPatterns"]}updateValidators(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([V.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([V.required,V.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})}}e("MsgDelayConfigComponent",$t),$t.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:$t,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),$t.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:$t,selector:"tb-action-node-msg-delay-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n \n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:$t,decorators:[{type:n,args:[{selector:"tb-action-node-msg-delay-config",template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n \n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Qt extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopes=Object.keys(u),this.telemetryTypeTranslationsMap=p}configForm(){return this.pushToCloudConfigForm}onConfigurationSet(e){this.pushToCloudConfigForm=this.fb.group({scope:[e?e.scope:null,[V.required]]})}}e("PushToCloudConfigComponent",Qt),Qt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Qt,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Qt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Qt,selector:"tb-action-node-push-to-cloud-config",usesInheritance:!0,ngImport:t,template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Qt,decorators:[{type:n,args:[{selector:"tb-action-node-push-to-cloud-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Jt extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopes=Object.keys(u),this.telemetryTypeTranslationsMap=p}configForm(){return this.pushToEdgeConfigForm}onConfigurationSet(e){this.pushToEdgeConfigForm=this.fb.group({scope:[e?e.scope:null,[V.required]]})}}e("PushToEdgeConfigComponent",Jt),Jt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Jt,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Jt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Jt,selector:"tb-action-node-push-to-edge-config",usesInheritance:!0,ngImport:t,template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Jt,decorators:[{type:n,args:[{selector:"tb-action-node-push-to-edge-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Yt extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.rpcReplyConfigForm}onConfigurationSet(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})}}e("RpcReplyConfigComponent",Yt),Yt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Yt,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Yt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Yt,selector:"tb-action-node-rpc-reply-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n',dependencies:[{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Yt,decorators:[{type:n,args:[{selector:"tb-action-node-rpc-reply-config",template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Wt extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.rpcRequestConfigForm}onConfigurationSet(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[V.required,V.min(0)]]})}}e("RpcRequestConfigComponent",Wt),Wt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Wt,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Wt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Wt,selector:"tb-action-node-rpc-request-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Wt,decorators:[{type:n,args:[{selector:"tb-action-node-rpc-request-config",template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Xt extends h{get required(){return this.requiredValue}set required(e){this.requiredValue=fe(e)}constructor(e,t,n,r){super(e),this.store=e,this.translate=t,this.injector=n,this.fb=r,this.propagateChange=null,this.valueChangeSubscription=null}ngOnInit(){this.ngControl=this.injector.get(w),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))}keyValsFormArray(){return this.kvListFormGroup.get("keyVals")}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})}writeValue(e){this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();const t=[];if(e)for(const n of Object.keys(e))Object.prototype.hasOwnProperty.call(e,n)&&t.push(this.fb.group({key:[n,[V.required]],value:[e[n],[V.required]]}));this.kvListFormGroup.setControl("keyVals",this.fb.array(t)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((()=>{this.updateModel()}))}removeKeyVal(e){this.kvListFormGroup.get("keyVals").removeAt(e)}addKeyVal(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[V.required]],value:["",[V.required]]}))}validate(e){const t=this.kvListFormGroup.get("keyVals").value;if(!t.length&&this.required)return{kvMapRequired:!0};if(!this.kvListFormGroup.valid)return{kvFieldsRequired:!0};if(this.uniqueKeyValuePairValidator)for(const e of t)if(e.key===e.value)return{uniqueKeyValuePair:!0};return null}updateModel(){const e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{const t={};e.forEach((e=>{t[e.key]=e.value})),this.propagateChange(t)}}}e("KvMapConfigOldComponent",Xt),Xt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Xt,deps:[{token:E.Store},{token:$.TranslateService},{token:t.Injector},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Xt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Xt,selector:"tb-kv-map-config-old",inputs:{disabled:"disabled",uniqueKeyValuePairValidator:"uniqueKeyValuePairValidator",requiredText:"requiredText",keyText:"keyText",keyRequiredText:"keyRequiredText",valText:"valText",valRequiredText:"valRequiredText",hintText:"hintText",required:"required"},providers:[{provide:P,useExisting:i((()=>Xt)),multi:!0},{provide:R,useExisting:i((()=>Xt)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n
\n {{ keyText | translate }}\n {{ valText | translate }}\n \n
\n
\n
\n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n
\n \n
\n \n
\n
\n',styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:#757575;font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:0;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host .tb-kv-map-config tb-error{display:block;margin-top:-12px}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ge.TbErrorComponent,selector:"tb-error",inputs:["noMargin","error"]},{kind:"component",type:ae.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ae.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:ye.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:xe.DefaultShowHideDirective,selector:" [fxShow], [fxShow.print], [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl], [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl], [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg], [fxHide], [fxHide.print], [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl], [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl], [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg]",inputs:["fxShow","fxShow.print","fxShow.xs","fxShow.sm","fxShow.md","fxShow.lg","fxShow.xl","fxShow.lt-sm","fxShow.lt-md","fxShow.lt-lg","fxShow.lt-xl","fxShow.gt-xs","fxShow.gt-sm","fxShow.gt-md","fxShow.gt-lg","fxHide","fxHide.print","fxHide.xs","fxHide.sm","fxHide.md","fxHide.lg","fxHide.xl","fxHide.lt-sm","fxHide.lt-md","fxHide.lt-lg","fxHide.lt-xl","fxHide.gt-xs","fxHide.gt-sm","fxHide.gt-md","fxHide.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormControlDirective,selector:"[formControl]",inputs:["formControl","disabled","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormArrayName,selector:"[formArrayName]",inputs:["formArrayName"]},{kind:"pipe",type:K.AsyncPipe,name:"async"},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Xt,decorators:[{type:n,args:[{selector:"tb-kv-map-config-old",providers:[{provide:P,useExisting:i((()=>Xt)),multi:!0},{provide:R,useExisting:i((()=>Xt)),multi:!0}],template:'
\n
\n {{ keyText | translate }}\n {{ valText | translate }}\n \n
\n
\n
\n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n
\n \n
\n \n
\n
\n',styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:#757575;font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:0;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host .tb-kv-map-config tb-error{display:block;margin-top:-12px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:t.Injector},{type:D.UntypedFormBuilder}]},propDecorators:{disabled:[{type:l}],uniqueKeyValuePairValidator:[{type:l}],requiredText:[{type:l}],keyText:[{type:l}],keyRequiredText:[{type:l}],valText:[{type:l}],valRequiredText:[{type:l}],hintText:[{type:l}],required:[{type:l}]}});class Zt extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.saveToCustomTableConfigForm}onConfigurationSet(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[V.required,V.pattern(/.*\S.*/)]],fieldsMapping:[e?e.fieldsMapping:null,[V.required]]})}prepareOutputConfig(e){return e.tableName=e.tableName.trim(),e}}e("SaveToCustomTableConfigComponent",Zt),Zt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Zt,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Zt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Zt,selector:"tb-action-node-custom-table-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n tb.rulenode.custom-table-hint\n \n \n \n \n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Zt,decorators:[{type:n,args:[{selector:"tb-action-node-custom-table-config",template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n tb.rulenode.custom-table-hint\n \n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class en extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.timeseriesConfigForm}onConfigurationSet(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[V.required,V.min(0)]],skipLatestPersistence:[!!e&&e.skipLatestPersistence,[]],useServerTs:[!!e&&e.useServerTs,[]]})}}e("TimeseriesConfigComponent",en),en.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:en,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),en.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:en,selector:"tb-action-node-timeseries-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.skip-latest-persistence\' | translate }}\n \n \n {{ \'tb.rulenode.use-server-ts\' | translate }}\n \n
tb.rulenode.use-server-ts-hint
\n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:en,decorators:[{type:n,args:[{selector:"tb-action-node-timeseries-config",template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.skip-latest-persistence\' | translate }}\n \n \n {{ \'tb.rulenode.use-server-ts\' | translate }}\n \n
tb.rulenode.use-server-ts-hint
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class tn extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.unassignCustomerConfigForm}onConfigurationSet(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[V.required,V.pattern(/.*\S.*/)]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[V.required,V.min(0)]]})}prepareOutputConfig(e){return e.customerNamePattern=e.customerNamePattern.trim(),e}}e("UnassignCustomerConfigComponent",tn),tn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:tn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),tn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:tn,selector:"tb-action-node-un-assign-to-customer-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:tn,decorators:[{type:n,args:[{selector:"tb-action-node-un-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class nn extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopeMap=u,this.attributeScopes=Object.keys(u),this.telemetryTypeTranslationsMap=p,this.separatorKeysCodes=[le,se,me]}configForm(){return this.deleteAttributesConfigForm}onConfigurationSet(e){this.deleteAttributesConfigForm=this.fb.group({scope:[e?e.scope:null,[V.required]],keys:[e?e.keys:null,[V.required]],sendAttributesDeletedNotification:[!!e&&e.sendAttributesDeletedNotification,[]],notifyDevice:[!!e&&e.notifyDevice,[]]}),this.deleteAttributesConfigForm.get("scope").valueChanges.subscribe((e=>{e!==u.SHARED_SCOPE&&this.deleteAttributesConfigForm.get("notifyDevice").patchValue(!1,{emitEvent:!1})}))}removeKey(e){const t=this.deleteAttributesConfigForm.get("keys").value,n=t.indexOf(e);n>=0&&(t.splice(n,1),this.deleteAttributesConfigForm.get("keys").patchValue(t,{emitEvent:!0}))}addKey(e){const t=e.input;let n=e.value;if((n||"").trim()){n=n.trim();let e=this.deleteAttributesConfigForm.get("keys").value;e&&-1!==e.indexOf(n)||(e||(e=[]),e.push(n),this.deleteAttributesConfigForm.get("keys").patchValue(e,{emitEvent:!0}))}t&&(t.value="")}}e("DeleteAttributesConfigComponent",nn),nn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:nn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),nn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:nn,selector:"tb-action-node-delete-attributes-config",viewQueries:[{propertyName:"attributeChipList",first:!0,predicate:["attributeChipList"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'attribute.attributes-scope\' | translate }}\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n \n \n \n {{key}}\n close\n \n \n \n {{ \'tb.rulenode.attributes-keys-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.send-attributes-deleted-notification\' | translate }}\n \n
tb.rulenode.send-attributes-deleted-notification-hint
\n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-delete-hint
\n
\n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:pe.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:pe.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:pe.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:pe.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:nn,decorators:[{type:n,args:[{selector:"tb-action-node-delete-attributes-config",template:'
\n \n {{ \'attribute.attributes-scope\' | translate }}\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n \n \n \n {{key}}\n close\n \n \n \n {{ \'tb.rulenode.attributes-keys-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.send-attributes-deleted-notification\' | translate }}\n \n
tb.rulenode.send-attributes-deleted-notification-hint
\n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-delete-hint
\n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]},propDecorators:{attributeChipList:[{type:a,args:["attributeChipList"]}]}});class rn extends h{get function(){return this.functionValue}set function(e){e&&this.functionValue!==e&&(this.functionValue=e,this.setupArgumentsFormGroup(!0))}constructor(e,t,n,r){super(e),this.store=e,this.translate=t,this.injector=n,this.fb=r,this.maxArgs=16,this.minArgs=1,this.displayArgumentName=!1,this.mathFunctionMap=Dt,this.ArgumentType=Vt,this.attributeScopeMap=Ut,this.argumentTypeResultMap=Ot,this.arguments=Object.values(Vt),this.attributeScope=Object.values(Kt),this.propagateChange=null,this.valueChangeSubscription=[]}ngOnInit(){this.ngControl=this.injector.get(w),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.argumentsFormGroup=this.fb.group({arguments:this.fb.array([])}),this.valueChangeSubscription.push(this.argumentsFormGroup.valueChanges.subscribe((()=>{this.updateModel()}))),this.setupArgumentsFormGroup()}onDrop(e){const t=this.argumentsFormArray(),n=t.at(e.previousIndex);t.removeAt(e.previousIndex),t.insert(e.currentIndex,n),this.updateArgumentNames()}argumentsFormArray(){return this.argumentsFormGroup.get("arguments")}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.argumentsFormGroup.disable({emitEvent:!1}):(this.argumentsFormGroup.enable({emitEvent:!1}),this.argumentsFormGroup.get("arguments").controls.forEach((e=>this.updateArgumentControlValidators(e))))}ngOnDestroy(){this.valueChangeSubscription.length&&this.valueChangeSubscription.forEach((e=>e.unsubscribe()))}writeValue(e){const t=[];e&&e.forEach(((e,n)=>{t.push(this.createArgumentControl(e,n))})),this.argumentsFormGroup.setControl("arguments",this.fb.array(t),{emitEvent:!1}),this.setupArgumentsFormGroup()}removeArgument(e){this.argumentsFormGroup.get("arguments").removeAt(e),this.updateArgumentNames()}addArgument(e=!0){const t=this.argumentsFormGroup.get("arguments"),n=this.createArgumentControl(null,t.length);t.push(n,{emitEvent:e})}validate(e){return this.argumentsFormGroup.valid?null:{argumentsRequired:!0}}setupArgumentsFormGroup(e=!1){if(this.function&&(this.maxArgs=this.mathFunctionMap.get(this.function).maxArgs,this.minArgs=this.mathFunctionMap.get(this.function).minArgs,this.displayArgumentName=this.function===Et.CUSTOM),this.argumentsFormGroup){for(this.argumentsFormGroup.get("arguments").setValidators([V.minLength(this.minArgs),V.maxLength(this.maxArgs)]),this.argumentsFormGroup.get("arguments").value.length>this.maxArgs&&(this.argumentsFormGroup.get("arguments").controls.length=this.maxArgs);this.argumentsFormGroup.get("arguments").value.length{this.updateArgumentControlValidators(n),n.get("attributeScope").updateValueAndValidity({emitEvent:!1}),n.get("defaultValue").updateValueAndValidity({emitEvent:!1})}))),n}updateArgumentControlValidators(e){const t=e.get("type").value;t===Vt.ATTRIBUTE?e.get("attributeScope").enable({emitEvent:!1}):e.get("attributeScope").disable({emitEvent:!1}),t&&t!==Vt.CONSTANT?e.get("defaultValue").enable({emitEvent:!1}):e.get("defaultValue").disable({emitEvent:!1})}updateArgumentNames(){this.argumentsFormGroup.get("arguments").controls.forEach(((e,t)=>{e.get("name").setValue(Ht[t])}))}updateModel(){const e=this.argumentsFormGroup.get("arguments").value;e.length&&this.argumentsFormGroup.valid?this.propagateChange(e):this.propagateChange(null)}}e("ArgumentsMapConfigComponent",rn),rn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:rn,deps:[{token:E.Store},{token:$.TranslateService},{token:t.Injector},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),rn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:rn,selector:"tb-arguments-map-config",inputs:{disabled:"disabled",function:"function"},providers:[{provide:P,useExisting:i((()=>rn)),multi:!0},{provide:R,useExisting:i((()=>rn)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n\n
\n \n \n
\n \n
\n {{argumentControl.get(\'name\').value}}.\n
\n
\n \n tb.rulenode.argument-type-field-input\n \n \n {{ argumentTypeResultMap.get(argument) | translate }}\n \n \n \n tb.rulenode.argument-type-field-input-required\n \n \n \n tb.rulenode.argument-key-field-input\n \n help\n \n tb.rulenode.argument-key-field-input-required\n \n \n \n tb.rulenode.constant-value-field-input\n \n \n tb.rulenode.constant-value-field-input-required\n \n \n
\n
\n \n tb.rulenode.attribute-scope-field-input\n \n \n {{ attributeScopeMap.get(scope) | translate }}\n \n \n \n tb.rulenode.attribute-scope-field-input-required\n \n \n \n tb.rulenode.default-value-field-input\n \n \n
\n
\n \n
\n
\n
\n
\n
\n
\n tb.rulenode.no-arguments-prompt\n
\n \n
\n',styles:[":host .mat-mdc-list-item.tb-argument{border:solid rgba(0,0,0,.25) 1px;border-radius:4px;padding:10px 0;margin-bottom:16px}:host .arguments-list{padding:0}\n"],dependencies:[{kind:"directive",type:K.NgClass,selector:"[ngClass]",inputs:["class","ngClass"]},{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ae.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ae.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:ye.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"component",type:be.MatList,selector:"mat-list",exportAs:["matList"]},{kind:"component",type:be.MatListItem,selector:"mat-list-item, a[mat-list-item], button[mat-list-item]",inputs:["activated"],exportAs:["matListItem"]},{kind:"directive",type:he.CdkDropList,selector:"[cdkDropList], cdk-drop-list",inputs:["cdkDropListConnectedTo","cdkDropListData","cdkDropListOrientation","id","cdkDropListLockAxis","cdkDropListDisabled","cdkDropListSortingDisabled","cdkDropListEnterPredicate","cdkDropListSortPredicate","cdkDropListAutoScrollDisabled","cdkDropListAutoScrollStep"],outputs:["cdkDropListDropped","cdkDropListEntered","cdkDropListExited","cdkDropListSorted"],exportAs:["cdkDropList"]},{kind:"directive",type:he.CdkDrag,selector:"[cdkDrag]",inputs:["cdkDragData","cdkDragLockAxis","cdkDragRootElement","cdkDragBoundary","cdkDragStartDelay","cdkDragFreeDragPosition","cdkDragDisabled","cdkDragConstrainPosition","cdkDragPreviewClass","cdkDragPreviewContainer"],outputs:["cdkDragStarted","cdkDragReleased","cdkDragEnded","cdkDragEntered","cdkDragExited","cdkDragDropped","cdkDragMoved"],exportAs:["cdkDrag"]},{kind:"directive",type:he.CdkDragHandle,selector:"[cdkDragHandle]",inputs:["cdkDragHandleDisabled"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:xe.DefaultClassDirective,selector:" [ngClass], [ngClass.xs], [ngClass.sm], [ngClass.md], [ngClass.lg], [ngClass.xl], [ngClass.lt-sm], [ngClass.lt-md], [ngClass.lt-lg], [ngClass.lt-xl], [ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg]",inputs:["ngClass","ngClass.xs","ngClass.sm","ngClass.md","ngClass.lg","ngClass.xl","ngClass.lt-sm","ngClass.lt-md","ngClass.lt-lg","ngClass.lt-xl","ngClass.gt-xs","ngClass.gt-sm","ngClass.gt-md","ngClass.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormControlDirective,selector:"[formControl]",inputs:["formControl","disabled","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormArrayName,selector:"[formArrayName]",inputs:["formArrayName"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:rn,decorators:[{type:n,args:[{selector:"tb-arguments-map-config",providers:[{provide:P,useExisting:i((()=>rn)),multi:!0},{provide:R,useExisting:i((()=>rn)),multi:!0}],template:'
\n\n
\n \n \n
\n \n
\n {{argumentControl.get(\'name\').value}}.\n
\n
\n \n tb.rulenode.argument-type-field-input\n \n \n {{ argumentTypeResultMap.get(argument) | translate }}\n \n \n \n tb.rulenode.argument-type-field-input-required\n \n \n \n tb.rulenode.argument-key-field-input\n \n help\n \n tb.rulenode.argument-key-field-input-required\n \n \n \n tb.rulenode.constant-value-field-input\n \n \n tb.rulenode.constant-value-field-input-required\n \n \n
\n
\n \n tb.rulenode.attribute-scope-field-input\n \n \n {{ attributeScopeMap.get(scope) | translate }}\n \n \n \n tb.rulenode.attribute-scope-field-input-required\n \n \n \n tb.rulenode.default-value-field-input\n \n \n
\n
\n \n
\n
\n
\n
\n
\n
\n tb.rulenode.no-arguments-prompt\n
\n \n
\n',styles:[":host .mat-mdc-list-item.tb-argument{border:solid rgba(0,0,0,.25) 1px;border-radius:4px;padding:10px 0;margin-bottom:16px}:host .arguments-list{padding:0}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:t.Injector},{type:D.FormBuilder}]},propDecorators:{disabled:[{type:l}],function:[{type:l}]}});class on extends h{get required(){return this.requiredValue}set required(e){this.requiredValue=fe(e)}constructor(e,t,n,r){super(e),this.store=e,this.translate=t,this.injector=n,this.fb=r,this.searchText="",this.dirty=!1,this.mathOperation=[...Dt.values()],this.propagateChange=null}ngOnInit(){this.mathFunctionForm=this.fb.group({operation:[""]}),this.filteredOptions=this.mathFunctionForm.get("operation").valueChanges.pipe(Ce((e=>{let t;t="string"==typeof e&&Et[e]?Et[e]:null,this.updateView(t)})),ve((e=>(this.searchText=e||"",e?this._filter(e):this.mathOperation.slice()))))}_filter(e){const t=e.toLowerCase();return this.mathOperation.filter((e=>e.name.toLowerCase().includes(t)||e.value.toLowerCase().includes(t)))}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.mathFunctionForm.disable({emitEvent:!1}):this.mathFunctionForm.enable({emitEvent:!1})}mathFunctionDisplayFn(e){if(e){const t=Dt.get(e);return t.value+" | "+t.name}return""}writeValue(e){this.modelValue=e,this.mathFunctionForm.get("operation").setValue(e,{emitEvent:!1}),this.dirty=!0}updateView(e){this.modelValue!==e&&(this.modelValue=e,this.propagateChange(this.modelValue))}onFocus(){this.dirty&&(this.mathFunctionForm.get("operation").updateValueAndValidity({onlySelf:!0}),this.dirty=!1)}clear(){this.mathFunctionForm.get("operation").patchValue(""),setTimeout((()=>{this.operationInput.nativeElement.blur(),this.operationInput.nativeElement.focus()}),0)}}e("MathFunctionAutocompleteComponent",on),on.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:on,deps:[{token:E.Store},{token:$.TranslateService},{token:t.Injector},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),on.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:on,selector:"tb-math-function-autocomplete",inputs:{required:"required",disabled:"disabled"},providers:[{provide:P,useExisting:i((()=>on)),multi:!0}],viewQueries:[{propertyName:"operationInput",first:!0,predicate:["operationInput"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'\n tb.rulenode.functions-field-input\n \n \n \n \n \n \n {{ option.description }}\n \n \n \n tb.rulenode.no-option-found\n \n \n\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ae.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:Ie.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple","hideSingleSelectionIndicator"],exportAs:["matAutocomplete"]},{kind:"directive",type:Ie.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:K.AsyncPipe,name:"async"},{kind:"pipe",type:Ne.HighlightPipe,name:"highlight"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:on,decorators:[{type:n,args:[{selector:"tb-math-function-autocomplete",providers:[{provide:P,useExisting:i((()=>on)),multi:!0}],template:'\n tb.rulenode.functions-field-input\n \n \n \n \n \n \n {{ option.description }}\n \n \n \n tb.rulenode.no-option-found\n \n \n\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:t.Injector},{type:D.UntypedFormBuilder}]},propDecorators:{required:[{type:l}],disabled:[{type:l}],operationInput:[{type:a,args:["operationInput",{static:!0}]}]}});class an extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.MathFunction=Et,this.ArgumentTypeResult=wt,this.argumentTypeResultMap=Ot,this.attributeScopeMap=Ut,this.argumentsResult=Object.values(wt),this.attributeScopeResult=Object.values(Bt)}configForm(){return this.mathFunctionConfigForm}onConfigurationSet(e){this.mathFunctionConfigForm=this.fb.group({operation:[e?e.operation:null,[V.required]],arguments:[e?e.arguments:null,[V.required]],customFunction:[e?e.customFunction:"",[V.required]],result:this.fb.group({type:[e?e.result.type:null,[V.required]],attributeScope:[e?e.result.attributeScope:null,[V.required]],key:[e?e.result.key:"",[V.required]],resultValuePrecision:[e?e.result.resultValuePrecision:0],addToBody:[!!e&&e.result.addToBody],addToMetadata:[!!e&&e.result.addToMetadata]})})}updateValidators(e){const t=this.mathFunctionConfigForm.get("operation").value,n=this.mathFunctionConfigForm.get("result.type").value;t===Et.CUSTOM?this.mathFunctionConfigForm.get("customFunction").enable({emitEvent:!1}):this.mathFunctionConfigForm.get("customFunction").disable({emitEvent:!1}),n===wt.ATTRIBUTE?this.mathFunctionConfigForm.get("result.attributeScope").enable({emitEvent:!1}):this.mathFunctionConfigForm.get("result.attributeScope").disable({emitEvent:!1}),this.mathFunctionConfigForm.get("customFunction").updateValueAndValidity({emitEvent:e}),this.mathFunctionConfigForm.get("result.attributeScope").updateValueAndValidity({emitEvent:e})}validatorTriggers(){return["operation","result.type"]}}e("MathFunctionConfigComponent",an),an.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:an,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),an.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:an,selector:"tb-action-node-math-function-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n tb.rulenode.argument-tile\n \n \n
\n
\n {{\'tb.rulenode.custom-expression-field-input\' | translate }} *\n \n \n \n tb.rulenode.custom-expression-field-input-required\n \n \n \n
\n
\n tb.rulenode.result-title\n
\n
\n \n tb.rulenode.type-field-input\n \n \n {{ argumentTypeResultMap.get(argument) | translate }}\n \n \n \n tb.rulenode.type-field-input-required\n \n \n \n tb.rulenode.attribute-scope-field-input\n \n \n {{ attributeScopeMap.get(scope) | translate }}\n \n \n \n \n tb.rulenode.key-field-input\n \n help\n \n tb.rulenode.key-field-input-required\n \n \n
\n
\n \n tb.rulenode.number-floating-point-field-input\n \n \n \n
\n
\n
\n \n {{\'tb.rulenode.add-to-body-field-input\' | translate }}\n \n \n {{\'tb.rulenode.add-to-metadata-field-input\' | translate}}\n \n
\n
\n
\n
\n',styles:[":host ::ng-deep .fields-group{padding:0 16px 8px;margin:10px 0;border:1px groove rgba(0,0,0,.25);border-radius:4px}:host ::ng-deep .fields-group .mat-mdc-form-field .mat-mdc-form-field-infix{width:100%}:host ::ng-deep .fields-group legend{color:#000000b3;width:-moz-fit-content;width:fit-content}:host ::ng-deep .fields-group legend+*{display:block}:host ::ng-deep .fields-group legend+*.no-margin-top{margin-top:0}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:ye.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:D.FormGroupName,selector:"[formGroupName]",inputs:["formGroupName"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:rn,selector:"tb-arguments-map-config",inputs:["disabled","function"]},{kind:"component",type:on,selector:"tb-math-function-autocomplete",inputs:["required","disabled"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:an,decorators:[{type:n,args:[{selector:"tb-action-node-math-function-config",template:'
\n \n \n
\n tb.rulenode.argument-tile\n \n \n
\n
\n {{\'tb.rulenode.custom-expression-field-input\' | translate }} *\n \n \n \n tb.rulenode.custom-expression-field-input-required\n \n \n \n
\n
\n tb.rulenode.result-title\n
\n
\n \n tb.rulenode.type-field-input\n \n \n {{ argumentTypeResultMap.get(argument) | translate }}\n \n \n \n tb.rulenode.type-field-input-required\n \n \n \n tb.rulenode.attribute-scope-field-input\n \n \n {{ attributeScopeMap.get(scope) | translate }}\n \n \n \n \n tb.rulenode.key-field-input\n \n help\n \n tb.rulenode.key-field-input-required\n \n \n
\n
\n \n tb.rulenode.number-floating-point-field-input\n \n \n \n
\n
\n
\n \n {{\'tb.rulenode.add-to-body-field-input\' | translate }}\n \n \n {{\'tb.rulenode.add-to-metadata-field-input\' | translate}}\n \n
\n
\n
\n
\n',styles:[":host ::ng-deep .fields-group{padding:0 16px 8px;margin:10px 0;border:1px groove rgba(0,0,0,.25);border-radius:4px}:host ::ng-deep .fields-group .mat-mdc-form-field .mat-mdc-form-field-infix{width:100%}:host ::ng-deep .fields-group legend{color:#000000b3;width:-moz-fit-content;width:fit-content}:host ::ng-deep .fields-group legend+*{display:block}:host ::ng-deep .fields-group legend+*.no-margin-top{margin-top:0}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class ln{constructor(e,t){this.store=e,this.fb=t,this.subscriptSizing="fixed",this.searchText="",this.dirty=!1,this.messageTypes=["POST_ATTRIBUTES_REQUEST","POST_TELEMETRY_REQUEST"],this.propagateChange=e=>{},this.messageTypeFormGroup=this.fb.group({messageType:[null,[V.required,V.maxLength(255)]]})}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}ngOnInit(){this.outputMessageTypes=this.messageTypeFormGroup.get("messageType").valueChanges.pipe(Ce((e=>{this.updateView(e)})),ve((e=>e||"")),Fe((e=>this.fetchMessageTypes(e))))}writeValue(e){this.searchText="",this.modelValue=e,this.messageTypeFormGroup.get("messageType").patchValue(e,{emitEvent:!1}),this.dirty=!0}onFocus(){this.dirty&&(this.messageTypeFormGroup.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0}),this.dirty=!1)}updateView(e){this.modelValue!==e&&(this.modelValue=e,this.propagateChange(this.modelValue))}displayMessageTypeFn(e){return e||void 0}fetchMessageTypes(e,t=!1){return this.searchText=e,Se(this.messageTypes).pipe(ve((n=>n.filter((n=>t?!!e&&n===e:!e||n.toUpperCase().startsWith(e.toUpperCase()))))))}clear(){this.messageTypeFormGroup.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.messageTypeInput.nativeElement.blur(),this.messageTypeInput.nativeElement.focus()}),0)}}e("OutputMessageTypeAutocompleteComponent",ln),ln.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ln,deps:[{token:E.Store},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ln.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:ln,selector:"tb-output-message-type-autocomplete",inputs:{autocompleteHint:"autocompleteHint",subscriptSizing:"subscriptSizing"},providers:[{provide:P,useExisting:i((()=>ln)),multi:!0}],viewQueries:[{propertyName:"messageTypeInput",first:!0,predicate:["messageTypeInput"],descendants:!0,static:!0}],ngImport:t,template:'\n \n \n \n \n {{msgType}}\n \n \n {{autocompleteHint | translate}}\n \n {{ \'tb.rulenode.output-message-type-required\' | translate }}\n \n \n {{ \'tb.rulenode.output-message-type-max-length\' | translate }}\n \n\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ae.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:Ie.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple","hideSingleSelectionIndicator"],exportAs:["matAutocomplete"]},{kind:"directive",type:Ie.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"pipe",type:K.AsyncPipe,name:"async"},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ln,decorators:[{type:n,args:[{selector:"tb-output-message-type-autocomplete",providers:[{provide:P,useExisting:i((()=>ln)),multi:!0}],template:'\n \n \n \n \n {{msgType}}\n \n \n {{autocompleteHint | translate}}\n \n {{ \'tb.rulenode.output-message-type-required\' | translate }}\n \n \n {{ \'tb.rulenode.output-message-type-max-length\' | translate }}\n \n\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.FormBuilder}]},propDecorators:{messageTypeInput:[{type:a,args:["messageTypeInput",{static:!0}]}],autocompleteHint:[{type:l}],subscriptSizing:[{type:l}]}});class sn extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.destroy$=new qe,this.serviceType=d.TB_RULE_ENGINE,this.deduplicationStrategie=yt,this.deduplicationStrategies=Object.keys(this.deduplicationStrategie),this.deduplicationStrategiesTranslations=xt}configForm(){return this.deduplicationConfigForm}onConfigurationSet(e){this.deduplicationConfigForm=this.fb.group({interval:[ee(e?.interval)?e.interval:null,[V.required,V.min(1)]],strategy:[ee(e?.strategy)?e.strategy:null,[V.required]],outMsgType:[ee(e?.outMsgType)?e.outMsgType:null,[V.required]],queueName:[ee(e?.queueName)?e.queueName:null,[V.required]],maxPendingMsgs:[ee(e?.maxPendingMsgs)?e.maxPendingMsgs:null,[V.required,V.min(1),V.max(1e3)]],maxRetries:[ee(e?.maxRetries)?e.maxRetries:null,[V.required,V.min(0),V.max(100)]]}),this.deduplicationConfigForm.get("strategy").valueChanges.pipe(Le(this.destroy$)).subscribe((e=>{this.enableControl(e)}))}updateValidators(e){this.enableControl(this.deduplicationConfigForm.get("strategy").value)}validatorTriggers(){return["strategy"]}enableControl(e){e===this.deduplicationStrategie.ALL?(this.deduplicationConfigForm.get("outMsgType").enable({emitEvent:!1}),this.deduplicationConfigForm.get("queueName").enable({emitEvent:!1})):(this.deduplicationConfigForm.get("outMsgType").disable({emitEvent:!1}),this.deduplicationConfigForm.get("queueName").disable({emitEvent:!1}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}}e("DeduplicationConfigComponent",sn),sn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:sn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),sn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:sn,selector:"tb-action-node-msg-deduplication-config",usesInheritance:!0,ngImport:t,template:"
\n \n {{'tb.rulenode.interval' | translate}}\n \n {{'tb.rulenode.interval-hint' | translate}}\n \n {{'tb.rulenode.interval-required' | translate}}\n \n \n {{'tb.rulenode.interval-min-error' | translate}}\n \n \n \n {{'tb.rulenode.strategy' | translate}}\n \n \n {{ deduplicationStrategiesTranslations.get(strategy) | translate }}\n \n \n \n {{'tb.rulenode.strategy-first-hint' | translate}}\n {{'tb.rulenode.strategy-last-hint' | translate}}\n \n {{'tb.rulenode.strategy-required' | translate}}\n \n \n
\n \n \n \n \n
\n \n \n \n
\n
Advanced settings
\n
\n
\n
\n \n \n {{'tb.rulenode.max-pending-msgs' | translate}}\n \n {{'tb.rulenode.max-pending-msgs-hint' | translate}}\n \n {{'tb.rulenode.max-pending-msgs-required' | translate}}\n \n \n {{'tb.rulenode.max-pending-msgs-max-error' | translate}}\n \n \n {{'tb.rulenode.max-pending-msgs-min-error' | translate}}\n \n \n \n {{'tb.rulenode.max-retries' | translate}}\n \n {{'tb.rulenode.max-retries-hint' | translate}}\n \n {{'tb.rulenode.max-retries-required' | translate}}\n \n \n {{'tb.rulenode.max-retries-max-error' | translate}}\n \n \n {{'tb.rulenode.max-retries-min-error' | translate}}\n \n \n \n
\n
\n",styles:[":host ::ng-deep .mat-expansion-panel.advanced-settings{border:none;box-shadow:none;padding:0}:host ::ng-deep .mat-expansion-panel.advanced-settings .mat-expansion-panel-body{padding:0}:host ::ng-deep .mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:not([aria-disabled=true]):hover{background:white}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:W.QueueAutocompleteComponent,selector:"tb-queue-autocomplete",inputs:["labelText","requiredText","autocompleteHint","subscriptSizing","required","queueType","disabled"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:Me.MatExpansionPanel,selector:"mat-expansion-panel",inputs:["disabled","expanded","hideToggle","togglePosition"],outputs:["opened","closed","expandedChange","afterExpand","afterCollapse"],exportAs:["matExpansionPanel"]},{kind:"component",type:Me.MatExpansionPanelHeader,selector:"mat-expansion-panel-header",inputs:["tabIndex","expandedHeight","collapsedHeight"]},{kind:"directive",type:Me.MatExpansionPanelTitle,selector:"mat-panel-title"},{kind:"directive",type:Me.MatExpansionPanelContent,selector:"ng-template[matExpansionPanelContent]"},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:ln,selector:"tb-output-message-type-autocomplete",inputs:["autocompleteHint","subscriptSizing"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:sn,decorators:[{type:n,args:[{selector:"tb-action-node-msg-deduplication-config",template:"
\n \n {{'tb.rulenode.interval' | translate}}\n \n {{'tb.rulenode.interval-hint' | translate}}\n \n {{'tb.rulenode.interval-required' | translate}}\n \n \n {{'tb.rulenode.interval-min-error' | translate}}\n \n \n \n {{'tb.rulenode.strategy' | translate}}\n \n \n {{ deduplicationStrategiesTranslations.get(strategy) | translate }}\n \n \n \n {{'tb.rulenode.strategy-first-hint' | translate}}\n {{'tb.rulenode.strategy-last-hint' | translate}}\n \n {{'tb.rulenode.strategy-required' | translate}}\n \n \n
\n \n \n \n \n
\n \n \n \n
\n
Advanced settings
\n
\n
\n
\n \n \n {{'tb.rulenode.max-pending-msgs' | translate}}\n \n {{'tb.rulenode.max-pending-msgs-hint' | translate}}\n \n {{'tb.rulenode.max-pending-msgs-required' | translate}}\n \n \n {{'tb.rulenode.max-pending-msgs-max-error' | translate}}\n \n \n {{'tb.rulenode.max-pending-msgs-min-error' | translate}}\n \n \n \n {{'tb.rulenode.max-retries' | translate}}\n \n {{'tb.rulenode.max-retries-hint' | translate}}\n \n {{'tb.rulenode.max-retries-required' | translate}}\n \n \n {{'tb.rulenode.max-retries-max-error' | translate}}\n \n \n {{'tb.rulenode.max-retries-min-error' | translate}}\n \n \n \n
\n
\n",styles:[":host ::ng-deep .mat-expansion-panel.advanced-settings{border:none;box-shadow:none;padding:0}:host ::ng-deep .mat-expansion-panel.advanced-settings .mat-expansion-panel-body{padding:0}:host ::ng-deep .mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:not([aria-disabled=true]):hover{background:white}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class mn extends h{constructor(e,t,n,r){super(e),this.store=e,this.translate=t,this.injector=n,this.fb=r,this.propagateChange=null,this.valueChangeSubscription=null,this.disabled=!1,this.uniqueKeyValuePairValidator=!1,this.required=!1}ngOnInit(){this.ngControl=this.injector.get(w),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))}keyValsFormArray(){return this.kvListFormGroup.get("keyVals")}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})}writeValue(e){this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();const t=[];if(e)for(const n of Object.keys(e))Object.prototype.hasOwnProperty.call(e,n)&&t.push(this.fb.group({key:[n,[V.required,V.pattern(/(?:.|\s)*\S(&:.|\s)*/)]],value:[e[n],[V.required,V.pattern(/(?:.|\s)*\S(&:.|\s)*/)]]}));this.kvListFormGroup.setControl("keyVals",this.fb.array(t)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((()=>{this.updateModel()}))}removeKeyVal(e){this.kvListFormGroup.get("keyVals").removeAt(e)}addKeyVal(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[V.required,V.pattern(/(?:.|\s)*\S(&:.|\s)*/)]],value:["",[V.required,V.pattern(/(?:.|\s)*\S(&:.|\s)*/)]]}))}validate(e){const t=this.kvListFormGroup.get("keyVals").value;if(!t.length&&this.required)return{kvMapRequired:!0};if(!this.kvListFormGroup.valid)return{kvFieldsRequired:!0};if(this.uniqueKeyValuePairValidator)for(const e of t)if(e.key===e.value)return{uniqueKeyValuePair:!0};return null}updateModel(){const e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{const t={};e.forEach((e=>{t[e.key]=e.value})),this.propagateChange(t)}}}e("KvMapConfigComponent",mn),mn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:mn,deps:[{token:E.Store},{token:$.TranslateService},{token:t.Injector},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),mn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:mn,selector:"tb-kv-map-config",inputs:{disabled:"disabled",uniqueKeyValuePairValidator:"uniqueKeyValuePairValidator",labelText:"labelText",requiredText:"requiredText",keyText:"keyText",keyRequiredText:"keyRequiredText",valText:"valText",valRequiredText:"valRequiredText",hintText:"hintText",popupHelpLink:"popupHelpLink",required:"required"},providers:[{provide:P,useExisting:i((()=>mn)),multi:!0},{provide:R,useExisting:i((()=>mn)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n
\n \n
\n
\n
\n
\n \n {{ keyText }}\n \n \n {{ keyRequiredText }}\n \n \n arrow_forward\n \n {{ valText }}\n \n \n {{ valRequiredText }}\n \n \n
\n \n
\n
\n
\n {{ \'tb.rulenode.kv-map-pattern-hint\' | translate }}\n \n
\n
\n
\n \n \n \n
\n \n
\n
\n',styles:[":host ::ng-deep{width:100%}:host ::ng-deep .tb-kv-map-config{margin-bottom:12px}:host ::ng-deep .tb-kv-map-config .map-label{font-weight:400;font-size:12px;color:#3d3d3d;letter-spacing:.25px}:host ::ng-deep .tb-kv-map-config .body{margin-top:7px;max-height:363px;overflow:auto}:host ::ng-deep .tb-kv-map-config .body .mapping-block{margin-bottom:15px}:host ::ng-deep .tb-kv-map-config .body .mapping-block .inputs-block{border:1px solid #E0E0E0;width:100%;border-radius:6px;padding:22px 22px 0;align-items:center}:host ::ng-deep .tb-kv-map-config .body .mapping-block .inputs-block .arrow-icon{width:24px;height:24px;line-height:24px;font-size:24px;margin:0 2px 22px;color:#9e9e9e}:host ::ng-deep .tb-kv-map-config tb-error{display:block;margin-top:-12px;margin-bottom:8px}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:Ee.HelpPopupComponent,selector:"[tb-help-popup], [tb-help-popup-content]",inputs:["tb-help-popup","tb-help-popup-content","trigger-text","trigger-style","tb-help-popup-placement","tb-help-popup-style"]},{kind:"component",type:ge.TbErrorComponent,selector:"tb-error",inputs:["noMargin","error"]},{kind:"component",type:ae.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ae.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:ye.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:xe.DefaultShowHideDirective,selector:" [fxShow], [fxShow.print], [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl], [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl], [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg], [fxHide], [fxHide.print], [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl], [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl], [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg]",inputs:["fxShow","fxShow.print","fxShow.xs","fxShow.sm","fxShow.md","fxShow.lg","fxShow.xl","fxShow.lt-sm","fxShow.lt-md","fxShow.lt-lg","fxShow.lt-xl","fxShow.gt-xs","fxShow.gt-sm","fxShow.gt-md","fxShow.gt-lg","fxHide","fxHide.print","fxHide.xs","fxHide.sm","fxHide.md","fxHide.lg","fxHide.xl","fxHide.lt-sm","fxHide.lt-md","fxHide.lt-lg","fxHide.lt-xl","fxHide.gt-xs","fxHide.gt-sm","fxHide.gt-md","fxHide.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormControlDirective,selector:"[formControl]",inputs:["formControl","disabled","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormArrayName,selector:"[formArrayName]",inputs:["formArrayName"]},{kind:"pipe",type:K.AsyncPipe,name:"async"},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),Ge([C()],mn.prototype,"disabled",void 0),Ge([C()],mn.prototype,"uniqueKeyValuePairValidator",void 0),Ge([C()],mn.prototype,"required",void 0),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:mn,decorators:[{type:n,args:[{selector:"tb-kv-map-config",providers:[{provide:P,useExisting:i((()=>mn)),multi:!0},{provide:R,useExisting:i((()=>mn)),multi:!0}],template:'
\n
\n \n
\n
\n
\n
\n \n {{ keyText }}\n \n \n {{ keyRequiredText }}\n \n \n arrow_forward\n \n {{ valText }}\n \n \n {{ valRequiredText }}\n \n \n
\n \n
\n
\n
\n {{ \'tb.rulenode.kv-map-pattern-hint\' | translate }}\n \n
\n
\n
\n \n \n \n
\n \n
\n
\n',styles:[":host ::ng-deep{width:100%}:host ::ng-deep .tb-kv-map-config{margin-bottom:12px}:host ::ng-deep .tb-kv-map-config .map-label{font-weight:400;font-size:12px;color:#3d3d3d;letter-spacing:.25px}:host ::ng-deep .tb-kv-map-config .body{margin-top:7px;max-height:363px;overflow:auto}:host ::ng-deep .tb-kv-map-config .body .mapping-block{margin-bottom:15px}:host ::ng-deep .tb-kv-map-config .body .mapping-block .inputs-block{border:1px solid #E0E0E0;width:100%;border-radius:6px;padding:22px 22px 0;align-items:center}:host ::ng-deep .tb-kv-map-config .body .mapping-block .inputs-block .arrow-icon{width:24px;height:24px;line-height:24px;font-size:24px;margin:0 2px 22px;color:#9e9e9e}:host ::ng-deep .tb-kv-map-config tb-error{display:block;margin-top:-12px;margin-bottom:8px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:t.Injector},{type:D.FormBuilder}]},propDecorators:{disabled:[{type:l}],uniqueKeyValuePairValidator:[{type:l}],labelText:[{type:l}],requiredText:[{type:l}],keyText:[{type:l}],keyRequiredText:[{type:l}],valText:[{type:l}],valRequiredText:[{type:l}],hintText:[{type:l}],popupHelpLink:[{type:l}],required:[{type:l}]}});class un{constructor(e,t){this.store=e,this.fb=t,this.destroy$=new qe}ngOnInit(){this.slideToggleControlGroup=this.fb.group({slideToggleControl:[null,[]]}),this.slideToggleControlGroup.get("slideToggleControl").valueChanges.pipe(Le(this.destroy$)).subscribe((e=>{this.propagateChange(e)}))}writeValue(e){this.slideToggleControlGroup.get("slideToggleControl").patchValue(e,{emitEvent:!1})}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){e?this.slideToggleControlGroup.disable({emitEvent:!1}):this.slideToggleControlGroup.enable({emitEvent:!1})}ngOnDestroy(){this.destroy$.next(null),this.destroy$.complete()}}e("SlideToggleComponent",un),un.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:un,deps:[{token:E.Store},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),un.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:un,selector:"tb-slide-toggle",inputs:{slideToggleName:"slideToggleName",slideToggleTooltip:"slideToggleTooltip"},providers:[{provide:P,useExisting:i((()=>un)),multi:!0}],ngImport:t,template:'
\n \n {{ slideToggleName }}\n \n info\n
\n',styles:[":host ::ng-deep .slide-toggle-container{align-items:center}:host ::ng-deep .slide-toggle-container .slide-toggle{margin-right:8px}:host ::ng-deep .slide-toggle-container .slide-toggle label{padding-left:12px}:host ::ng-deep .slide-toggle-container .tooltip-icon{width:18px;height:18px;line-height:18px;font-size:18px;color:#e0e0e0}:host ::ng-deep .slide-toggle-container .tooltip-icon:hover{color:#9e9e9e}\n"],dependencies:[{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:ye.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"component",type:we.MatSlideToggle,selector:"mat-slide-toggle",inputs:["disabled","disableRipple","color","tabIndex"],exportAs:["matSlideToggle"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:un,decorators:[{type:n,args:[{selector:"tb-slide-toggle",providers:[{provide:P,useExisting:i((()=>un)),multi:!0}],template:'
\n \n {{ slideToggleName }}\n \n info\n
\n',styles:[":host ::ng-deep .slide-toggle-container{align-items:center}:host ::ng-deep .slide-toggle-container .slide-toggle{margin-right:8px}:host ::ng-deep .slide-toggle-container .slide-toggle label{padding-left:12px}:host ::ng-deep .slide-toggle-container .tooltip-icon{width:18px;height:18px;line-height:18px;font-size:18px;color:#e0e0e0}:host ::ng-deep .slide-toggle-container .tooltip-icon:hover{color:#9e9e9e}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.FormBuilder}]},propDecorators:{slideToggleName:[{type:l}],slideToggleTooltip:[{type:l}]}});class pn extends h{get required(){return this.requiredValue}set required(e){this.requiredValue=fe(e)}constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(y),this.directionTypeTranslations=x,this.entityType=b,this.propagateChange=null}ngOnInit(){this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[V.required]],maxLevel:[null,[V.min(1)]],relationType:[null],deviceTypes:[null,[V.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((e=>{this.deviceRelationsQueryFormGroup.valid?this.propagateChange(e):this.propagateChange(null)}))}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})}writeValue(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})}}e("DeviceRelationsQueryConfigComponent",pn),pn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:pn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),pn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:pn,selector:"tb-device-relations-query-config",inputs:{disabled:"disabled",required:"required"},providers:[{provide:P,useExisting:i((()=>pn)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n {{ \'tb.rulenode.max-relation-level-error\' | translate }}\n \n \n
\n \n \n \n \n \n \n
\n',styles:[":host .relation-level{margin-bottom:16px}:host .last-level-slide-toggle{margin:8px 0 24px}:host .relation-type-autocomplete{margin-bottom:16px}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:De.EntitySubTypeListComponent,selector:"tb-entity-subtype-list",inputs:["label","required","disabled","entityType"]},{kind:"component",type:Ve.RelationTypeAutocompleteComponent,selector:"tb-relation-type-autocomplete",inputs:["label","floatLabel","required","disabled","subscriptSizing"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:un,selector:"tb-slide-toggle",inputs:["slideToggleName","slideToggleTooltip"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:pn,decorators:[{type:n,args:[{selector:"tb-device-relations-query-config",providers:[{provide:P,useExisting:i((()=>pn)),multi:!0}],template:'
\n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n {{ \'tb.rulenode.max-relation-level-error\' | translate }}\n \n \n
\n \n \n \n \n \n \n
\n',styles:[":host .relation-level{margin-bottom:16px}:host .last-level-slide-toggle{margin:8px 0 24px}:host .relation-type-autocomplete{margin-bottom:16px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]},propDecorators:{disabled:[{type:l}],required:[{type:l}]}});class dn{constructor(){this.required=!1}}e("FieldsetComponent",dn),dn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:dn,deps:[],target:t.ɵɵFactoryTarget.Component}),dn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:dn,selector:"tb-fieldset-component",inputs:{label:"label",required:"required"},ngImport:t,template:'
\n {{ label }}{{ required ? \'*\' : \'\' }}\n
\n \n
\n
\n',styles:[".fields-group{padding:0 16px;margin:0;border:1px solid #E0E0E0;border-radius:4px}.fields-group .fieldset-content{align-items:center}.fields-group .mat-mdc-form-field .mat-mdc-form-field-infix{width:100%}.fields-group legend{color:#757575;width:-moz-fit-content;width:fit-content;margin-bottom:4px}.fields-group legend+*{display:block}.fields-group legend+*.no-margin-top{margin-top:0}\n"],dependencies:[{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]}]}),Ge([C()],dn.prototype,"required",void 0),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:dn,decorators:[{type:n,args:[{selector:"tb-fieldset-component",template:'
\n {{ label }}{{ required ? \'*\' : \'\' }}\n
\n \n
\n
\n',styles:[".fields-group{padding:0 16px;margin:0;border:1px solid #E0E0E0;border-radius:4px}.fields-group .fieldset-content{align-items:center}.fields-group .mat-mdc-form-field .mat-mdc-form-field-infix{width:100%}.fields-group legend{color:#757575;width:-moz-fit-content;width:fit-content;margin-bottom:4px}.fields-group legend+*{display:block}.fields-group legend+*.no-margin-top{margin-top:0}\n"]}]}],propDecorators:{label:[{type:l}],required:[{type:l}]}});class cn extends h{get required(){return this.requiredValue}set required(e){this.requiredValue=fe(e)}constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(y),this.directionTypeTranslations=x,this.propagateChange=null}ngOnInit(){this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[V.required]],maxLevel:[null,[V.min(1)]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((e=>{this.relationsQueryFormGroup.valid?this.propagateChange(e):this.propagateChange(null)}))}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})}writeValue(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})}}e("RelationsQueryConfigComponent",cn),cn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:cn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),cn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:cn,selector:"tb-relations-query-config",inputs:{disabled:"disabled",required:"required"},providers:[{provide:P,useExisting:i((()=>cn)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n
\n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n {{ \'tb.rulenode.max-relation-level-error\' | translate }}\n \n \n
\n
\n \n \n \n
\n \n
\n
\n
\n',styles:[":host .map-label{font-weight:400;font-size:12px;color:#3d3d3d;letter-spacing:.25px}:host .last-level-slide-toggle{margin-bottom:18px;display:inline-block}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Pe.RelationFiltersComponent,selector:"tb-relation-filters",inputs:["disabled","allowedEntityTypes"]},{kind:"component",type:un,selector:"tb-slide-toggle",inputs:["slideToggleName","slideToggleTooltip"]},{kind:"component",type:dn,selector:"tb-fieldset-component",inputs:["label","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:cn,decorators:[{type:n,args:[{selector:"tb-relations-query-config",providers:[{provide:P,useExisting:i((()=>cn)),multi:!0}],template:'
\n \n
\n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n {{ \'tb.rulenode.max-relation-level-error\' | translate }}\n \n \n
\n
\n \n \n \n
\n \n
\n
\n
\n',styles:[":host .map-label{font-weight:400;font-size:12px;color:#3d3d3d;letter-spacing:.25px}:host .last-level-slide-toggle{margin-bottom:18px;display:inline-block}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]},propDecorators:{disabled:[{type:l}],required:[{type:l}]}});class fn extends h{get required(){return this.requiredValue}set required(e){this.requiredValue=fe(e)}constructor(e,t,n,r){super(e),this.store=e,this.translate=t,this.truncate=n,this.fb=r,this.placeholder="tb.rulenode.message-type",this.separatorKeysCodes=[le,se,me],this.messageTypes=[],this.messageTypesList=[],this.searchText="",this.propagateChange=e=>{},this.messageTypeConfigForm=this.fb.group({messageType:[null]});for(const e of Object.keys(v))this.messageTypesList.push({name:F.get(v[e]),value:e})}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}ngOnInit(){this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(ke(""),ve((e=>e||"")),Fe((e=>this.fetchMessageTypes(e))),Te())}ngAfterViewInit(){}setDisabledState(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})}writeValue(e){this.searchText="",this.messageTypes.length=0,e&&e.forEach((e=>{const t=this.messageTypesList.find((t=>t.value===e));t?this.messageTypes.push({name:t.name,value:t.value}):this.messageTypes.push({name:e,value:e})}))}displayMessageTypeFn(e){return e?e.name:void 0}textIsNotEmpty(e){return!!(e&&null!=e&&e.length>0)}createMessageType(e,t){e.preventDefault(),this.transformMessageType(t)}add(e){this.transformMessageType(e.value)}fetchMessageTypes(e){if(this.searchText=e,this.searchText&&this.searchText.length){const e=this.searchText.toUpperCase();return Se(this.messageTypesList.filter((t=>t.name.toUpperCase().includes(e))))}return Se(this.messageTypesList)}transformMessageType(e){if((e||"").trim()){let t=null;const n=e.trim(),r=this.messageTypesList.find((e=>e.name===n));t=r?{name:r.name,value:r.value}:{name:n,value:n},t&&this.addMessageType(t)}this.clear("")}remove(e){const t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())}selected(e){this.addMessageType(e.option.value),this.clear("")}addMessageType(e){-1===this.messageTypes.findIndex((t=>t.value===e.value))&&(this.messageTypes.push(e),this.updateModel())}onFocus(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})}clear(e=""){this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.messageTypeInput.nativeElement.blur(),this.messageTypeInput.nativeElement.focus()}),0)}updateModel(){const e=this.messageTypes.map((e=>e.value));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))}}e("MessageTypesConfigComponent",fn),fn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:fn,deps:[{token:E.Store},{token:$.TranslateService},{token:L.TruncatePipe},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),fn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:fn,selector:"tb-message-types-config",inputs:{required:"required",label:"label",placeholder:"placeholder",disabled:"disabled"},providers:[{provide:P,useExisting:i((()=>fn)),multi:!0}],viewQueries:[{propertyName:"chipList",first:!0,predicate:["chipList"],descendants:!0},{propertyName:"matAutocomplete",first:!0,predicate:["messageTypeAutocomplete"],descendants:!0},{propertyName:"messageTypeInput",first:!0,predicate:["messageTypeInput"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ \'tb.rulenode.no-message-type-matching\' | translate :\n { messageType: truncate.transform(searchText, true, 6, '...')}\n }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n \n {{ \'tb.rulenode.message-types-required\' | translate }}\n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:Ie.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple","hideSingleSelectionIndicator"],exportAs:["matAutocomplete"]},{kind:"directive",type:Ie.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{kind:"directive",type:Ie.MatAutocompleteOrigin,selector:"[matAutocompleteOrigin]",exportAs:["matAutocompleteOrigin"]},{kind:"component",type:pe.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:pe.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:pe.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:pe.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:K.AsyncPipe,name:"async"},{kind:"pipe",type:Ne.HighlightPipe,name:"highlight"},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:fn,decorators:[{type:n,args:[{selector:"tb-message-types-config",providers:[{provide:P,useExisting:i((()=>fn)),multi:!0}],template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ \'tb.rulenode.no-message-type-matching\' | translate :\n { messageType: truncate.transform(searchText, true, 6, '...')}\n }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n \n {{ \'tb.rulenode.message-types-required\' | translate }}\n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:L.TruncatePipe},{type:D.FormBuilder}]},propDecorators:{required:[{type:l}],label:[{type:l}],placeholder:[{type:l}],disabled:[{type:l}],chipList:[{type:a,args:["chipList",{static:!1}]}],matAutocomplete:[{type:a,args:["messageTypeAutocomplete",{static:!1}]}],messageTypeInput:[{type:a,args:["messageTypeInput",{static:!1}]}]}});class gn extends h{get required(){return this.requiredValue}set required(e){this.requiredValue=fe(e)}constructor(e,t){super(e),this.store=e,this.fb=t,this.subscriptions=[],this.disableCertPemCredentials=!1,this.passwordFieldRequired=!0,this.allCredentialsTypes=It,this.credentialsTypeTranslationsMap=Nt,this.propagateChange=e=>{}}ngOnInit(){this.credentialsConfigFormGroup=this.fb.group({type:[null,[V.required]],username:[null,[]],password:[null,[]],caCert:[null,[]],caCertFileName:[null,[]],privateKey:[null,[]],privateKeyFileName:[null,[]],cert:[null,[]],certFileName:[null,[]]}),this.subscriptions.push(this.credentialsConfigFormGroup.valueChanges.subscribe((()=>{this.updateView()}))),this.subscriptions.push(this.credentialsConfigFormGroup.get("type").valueChanges.subscribe((()=>{this.credentialsTypeChanged()})))}ngOnChanges(e){for(const t of Object.keys(e)){const n=e[t];if(!n.firstChange&&n.currentValue!==n.previousValue&&n.currentValue&&"disableCertPemCredentials"===t){"cert.PEM"===this.credentialsConfigFormGroup.get("type").value&&setTimeout((()=>{this.credentialsConfigFormGroup.get("type").patchValue("anonymous",{emitEvent:!0})}))}}}ngOnDestroy(){this.subscriptions.forEach((e=>e.unsubscribe()))}writeValue(e){ee(e)&&(this.credentialsConfigFormGroup.reset(e,{emitEvent:!1}),this.updateValidators())}setDisabledState(e){e?this.credentialsConfigFormGroup.disable({emitEvent:!1}):(this.credentialsConfigFormGroup.enable({emitEvent:!1}),this.updateValidators())}updateView(){let e=this.credentialsConfigFormGroup.value;const t=e.type;switch(t){case"anonymous":e={type:t};break;case"basic":e={type:t,username:e.username,password:e.password};break;case"cert.PEM":delete e.username}this.propagateChange(e)}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}validate(e){return this.credentialsConfigFormGroup.valid?null:{credentialsConfig:{valid:!1}}}credentialsTypeChanged(){this.credentialsConfigFormGroup.patchValue({username:null,password:null,caCert:null,caCertFileName:null,privateKey:null,privateKeyFileName:null,cert:null,certFileName:null}),this.updateValidators()}updateValidators(e=!1){const t=this.credentialsConfigFormGroup.get("type").value;switch(e&&this.credentialsConfigFormGroup.reset({type:t},{emitEvent:!1}),this.credentialsConfigFormGroup.setValidators([]),this.credentialsConfigFormGroup.get("username").setValidators([]),this.credentialsConfigFormGroup.get("password").setValidators([]),t){case"anonymous":break;case"basic":this.credentialsConfigFormGroup.get("username").setValidators([V.required]),this.credentialsConfigFormGroup.get("password").setValidators(this.passwordFieldRequired?[V.required]:[]);break;case"cert.PEM":this.credentialsConfigFormGroup.setValidators([this.requiredFilesSelected(V.required,[["caCert","caCertFileName"],["privateKey","privateKeyFileName","cert","certFileName"]])])}this.credentialsConfigFormGroup.get("username").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.get("password").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.updateValueAndValidity({emitEvent:e})}requiredFilesSelected(e,t=null){return n=>{t||(t=[Object.keys(n.controls)]);return n?.controls&&t.some((t=>t.every((t=>!e(n.controls[t])))))?null:{notAllRequiredFilesSelected:!0}}}}e("CredentialsConfigComponent",gn),gn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:gn,deps:[{token:E.Store},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),gn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:gn,selector:"tb-credentials-config",inputs:{required:"required",disableCertPemCredentials:"disableCertPemCredentials",passwordFieldRequired:"passwordFieldRequired"},providers:[{provide:P,useExisting:i((()=>gn)),multi:!0},{provide:R,useExisting:i((()=>gn)),multi:!0}],usesInheritance:!0,usesOnChanges:!0,ngImport:t,template:'
\n \n \n tb.rulenode.credentials\n \n {{ credentialsTypeTranslationsMap.get(credentialsConfigFormGroup.get(\'type\').value) | translate }}\n \n \n \n \n tb.rulenode.credentials-type\n \n \n {{ credentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n
{{ \'tb.rulenode.credentials-pem-hint\' | translate }}
\n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.NgSwitch,selector:"[ngSwitch]",inputs:["ngSwitch"]},{kind:"directive",type:K.NgSwitchCase,selector:"[ngSwitchCase]",inputs:["ngSwitchCase"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:Me.MatExpansionPanel,selector:"mat-expansion-panel",inputs:["disabled","expanded","hideToggle","togglePosition"],outputs:["opened","closed","expandedChange","afterExpand","afterCollapse"],exportAs:["matExpansionPanel"]},{kind:"component",type:Me.MatExpansionPanelHeader,selector:"mat-expansion-panel-header",inputs:["tabIndex","expandedHeight","collapsedHeight"]},{kind:"directive",type:Me.MatExpansionPanelTitle,selector:"mat-panel-title"},{kind:"directive",type:Me.MatExpansionPanelDescription,selector:"mat-panel-description"},{kind:"directive",type:Me.MatExpansionPanelContent,selector:"ng-template[matExpansionPanelContent]"},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:Re.FileInputComponent,selector:"tb-file-input",inputs:["label","accept","noFileText","inputId","allowedExtensions","dropLabel","contentConvertFunction","required","requiredAsError","disabled","existingFileName","readAsBinary","workFromFileObj","multipleFile"],outputs:["fileNameChanged"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Oe.TogglePasswordComponent,selector:"tb-toggle-password"},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:gn,decorators:[{type:n,args:[{selector:"tb-credentials-config",providers:[{provide:P,useExisting:i((()=>gn)),multi:!0},{provide:R,useExisting:i((()=>gn)),multi:!0}],template:'
\n \n \n tb.rulenode.credentials\n \n {{ credentialsTypeTranslationsMap.get(credentialsConfigFormGroup.get(\'type\').value) | translate }}\n \n \n \n \n tb.rulenode.credentials-type\n \n \n {{ credentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n
{{ \'tb.rulenode.credentials-pem-hint\' | translate }}
\n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.FormBuilder}]},propDecorators:{required:[{type:l}],disableCertPemCredentials:[{type:l}],passwordFieldRequired:[{type:l}]}});class yn{constructor(e,t,n){this.store=e,this.fb=t,this.translate=n,this.destroy$=new qe,this.selectOptions=[];for(const e of Rt.keys())this.selectOptions.push({value:e,name:this.translate.instant(Rt.get(e))})}ngOnInit(){this.chipControlGroup=this.fb.group({chipControl:[null,[]]}),this.chipControlGroup.get("chipControl").valueChanges.pipe(Le(this.destroy$)).subscribe((e=>{e&&this.propagateChange(e)}))}writeValue(e){this.chipControlGroup.get("chipControl").patchValue(e,{emitEvent:!1})}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){e?this.chipControlGroup.disable({emitEvent:!1}):this.chipControlGroup.enable({emitEvent:!1})}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}}e("MsgMetadataChipComponent",yn),yn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:yn,deps:[{token:E.Store},{token:D.FormBuilder},{token:$.TranslateService}],target:t.ɵɵFactoryTarget.Component}),yn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:yn,selector:"tb-msg-metadata-chip",inputs:{labelText:"labelText"},providers:[{provide:P,useExisting:i((()=>yn)),multi:!0}],ngImport:t,template:'
\n \n \n {{ option.name }}\n \n
\n',styles:[":host{width:100%}:host .chip-label{font-weight:400;font-size:12px;letter-spacing:.25px;color:#3d3d3d}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"component",type:pe.MatChipListbox,selector:"mat-chip-listbox",inputs:["tabIndex","multiple","aria-orientation","selectable","compareWith","required","hideSingleSelectionIndicator","value"],outputs:["change"]},{kind:"component",type:pe.MatChipOption,selector:"mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]",inputs:["color","disabled","disableRipple","tabIndex","selectable","selected"],outputs:["selectionChange"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:yn,decorators:[{type:n,args:[{selector:"tb-msg-metadata-chip",providers:[{provide:P,useExisting:i((()=>yn)),multi:!0}],template:'
\n \n \n {{ option.name }}\n \n
\n',styles:[":host{width:100%}:host .chip-label{font-weight:400;font-size:12px;letter-spacing:.25px;color:#3d3d3d}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.FormBuilder},{type:$.TranslateService}]},propDecorators:{labelText:[{type:l}]}});class xn extends h{constructor(e,t,n,r){super(e),this.store=e,this.translate=t,this.injector=n,this.fb=r,this.destroy$=new qe,this.sourceFieldSubcritption=[],this.propagateChange=null,this.valueChangeSubscription=null,this.disabled=!1,this.required=!1}ngOnInit(){this.ngControl=this.injector.get(w),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.svListFormGroup=this.fb.group({}),this.svListFormGroup.addControl("keyVals",this.fb.array([]))}keyValsFormArray(){return this.svListFormGroup.get("keyVals")}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.svListFormGroup.disable({emitEvent:!1}):this.svListFormGroup.enable({emitEvent:!1})}writeValue(e){this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();const t=[];if(e)for(const n of Object.keys(e))Object.prototype.hasOwnProperty.call(e,n)&&t.push(this.fb.group({key:[n,[V.required]],value:[e[n],[V.required,V.pattern(/(?:.|\s)*\S(&:.|\s)*/)]]}));this.svListFormGroup.setControl("keyVals",this.fb.array(t));for(const e of this.keyValsFormArray().controls)this.keyChangeSubscribe(e);this.valueChangeSubscription=this.svListFormGroup.valueChanges.pipe(Le(this.destroy$)).subscribe((e=>{this.updateModel()}))}filterSelectOptions(e){const t=[];for(const e of this.svListFormGroup.get("keyVals").value){const n=this.selectOptions.find((t=>t.value===e.key));n&&t.push(n)}const n=[];for(const r of this.selectOptions)ee(t.find((e=>e.value===r.value)))&&r.value!==e?.get("key").value||n.push(r);return n}removeKeyVal(e){this.svListFormGroup.get("keyVals").removeAt(e),this.sourceFieldSubcritption[e].unsubscribe(),this.sourceFieldSubcritption.splice(e,1)}addKeyVal(){const e=this.svListFormGroup.get("keyVals");e.push(this.fb.group({key:["",[V.required]],value:["",[V.required,V.pattern(/(?:.|\s)*\S(&:.|\s)*/)]]})),this.keyChangeSubscribe(e.controls[e.length-1])}keyChangeSubscribe(e){this.sourceFieldSubcritption.push(e.get("key").valueChanges.pipe(Le(this.destroy$)).subscribe((t=>{e.get("value").patchValue(this.targetKeyPrefix+t[0].toUpperCase()+t.slice(1))})))}validate(e){return!this.svListFormGroup.get("keyVals").value.length&&this.required?{svMapRequired:!0}:this.svListFormGroup.valid?null:{svFieldsRequired:!0}}updateModel(){const e=this.svListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.svListFormGroup.valid)this.propagateChange(null);else{const t={};e.forEach((e=>{t[e.key]=e.value})),this.propagateChange(t)}}}e("SvMapConfigComponent",xn),xn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:xn,deps:[{token:E.Store},{token:$.TranslateService},{token:t.Injector},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),xn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:xn,selector:"tb-sv-map-config",inputs:{selectOptions:"selectOptions",disabled:"disabled",labelText:"labelText",requiredText:"requiredText",targetKeyPrefix:"targetKeyPrefix",selectText:"selectText",selectRequiredText:"selectRequiredText",valText:"valText",valRequiredText:"valRequiredText",hintText:"hintText",popupHelpLink:"popupHelpLink",required:"required"},providers:[{provide:P,useExisting:i((()=>xn)),multi:!0},{provide:R,useExisting:i((()=>xn)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n
\n \n
\n
\n
\n
\n \n {{ selectText }}\n \n \n {{option.name}}\n \n \n \n {{ selectRequiredText }}\n \n \n arrow_forward\n \n {{ valText }}\n \n \n {{ valRequiredText }}\n \n \n
\n \n
\n
\n
\n {{ hintText }}\n \n
\n
\n \n \n
\n \n
\n',styles:[":host ::ng-deep{width:100%}:host ::ng-deep .tb-sv-map-config{margin-bottom:12px}:host ::ng-deep .tb-sv-map-config .map-label{font-weight:400;font-size:12px;color:#3d3d3d;letter-spacing:.25px}:host ::ng-deep .tb-sv-map-config .body{max-height:363px;overflow:auto;margin-top:7px}:host ::ng-deep .tb-sv-map-config .body .mapping-block{margin-bottom:15px}:host ::ng-deep .tb-sv-map-config .body .mapping-block .inputs-block{border:1px solid #E0E0E0;width:100%;border-radius:6px;padding:22px 22px 0;align-items:center}:host ::ng-deep .tb-sv-map-config .body .mapping-block .inputs-block .arrow-icon{width:24px;height:24px;line-height:24px;font-size:24px;margin:0 2px 22px;color:#9e9e9e}:host ::ng-deep .tb-sv-map-config tb-error{display:block;margin-top:-12px;margin-bottom:8px}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:Ee.HelpPopupComponent,selector:"[tb-help-popup], [tb-help-popup-content]",inputs:["tb-help-popup","tb-help-popup-content","trigger-text","trigger-style","tb-help-popup-placement","tb-help-popup-style"]},{kind:"component",type:ge.TbErrorComponent,selector:"tb-error",inputs:["noMargin","error"]},{kind:"component",type:ae.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ae.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:ye.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:xe.DefaultShowHideDirective,selector:" [fxShow], [fxShow.print], [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl], [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl], [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg], [fxHide], [fxHide.print], [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl], [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl], [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg]",inputs:["fxShow","fxShow.print","fxShow.xs","fxShow.sm","fxShow.md","fxShow.lg","fxShow.xl","fxShow.lt-sm","fxShow.lt-md","fxShow.lt-lg","fxShow.lt-xl","fxShow.gt-xs","fxShow.gt-sm","fxShow.gt-md","fxShow.gt-lg","fxHide","fxHide.print","fxHide.xs","fxHide.sm","fxHide.md","fxHide.lg","fxHide.xl","fxHide.lt-sm","fxHide.lt-md","fxHide.lt-lg","fxHide.lt-xl","fxHide.gt-xs","fxHide.gt-sm","fxHide.gt-md","fxHide.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormControlDirective,selector:"[formControl]",inputs:["formControl","disabled","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormArrayName,selector:"[formArrayName]",inputs:["formArrayName"]},{kind:"pipe",type:K.AsyncPipe,name:"async"},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),Ge([C()],xn.prototype,"disabled",void 0),Ge([C()],xn.prototype,"required",void 0),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:xn,decorators:[{type:n,args:[{selector:"tb-sv-map-config",providers:[{provide:P,useExisting:i((()=>xn)),multi:!0},{provide:R,useExisting:i((()=>xn)),multi:!0}],template:'
\n
\n \n
\n
\n
\n
\n \n {{ selectText }}\n \n \n {{option.name}}\n \n \n \n {{ selectRequiredText }}\n \n \n arrow_forward\n \n {{ valText }}\n \n \n {{ valRequiredText }}\n \n \n
\n \n
\n
\n
\n {{ hintText }}\n \n
\n
\n \n \n
\n \n
\n',styles:[":host ::ng-deep{width:100%}:host ::ng-deep .tb-sv-map-config{margin-bottom:12px}:host ::ng-deep .tb-sv-map-config .map-label{font-weight:400;font-size:12px;color:#3d3d3d;letter-spacing:.25px}:host ::ng-deep .tb-sv-map-config .body{max-height:363px;overflow:auto;margin-top:7px}:host ::ng-deep .tb-sv-map-config .body .mapping-block{margin-bottom:15px}:host ::ng-deep .tb-sv-map-config .body .mapping-block .inputs-block{border:1px solid #E0E0E0;width:100%;border-radius:6px;padding:22px 22px 0;align-items:center}:host ::ng-deep .tb-sv-map-config .body .mapping-block .inputs-block .arrow-icon{width:24px;height:24px;line-height:24px;font-size:24px;margin:0 2px 22px;color:#9e9e9e}:host ::ng-deep .tb-sv-map-config tb-error{display:block;margin-top:-12px;margin-bottom:8px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:t.Injector},{type:D.FormBuilder}]},propDecorators:{selectOptions:[{type:l}],disabled:[{type:l}],labelText:[{type:l}],requiredText:[{type:l}],targetKeyPrefix:[{type:l}],selectText:[{type:l}],selectRequiredText:[{type:l}],valText:[{type:l}],valRequiredText:[{type:l}],hintText:[{type:l}],popupHelpLink:[{type:l}],required:[{type:l}]}});class bn extends h{get required(){return this.requiredValue}set required(e){this.requiredValue=fe(e)}constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(y),this.directionTypeTranslations=x,this.propagateChange=null}ngOnInit(){this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[V.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((e=>{this.relationsQueryFormGroup.valid?this.propagateChange(e):this.propagateChange(null)}))}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})}writeValue(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})}}e("RelationsQueryConfigOldComponent",bn),bn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:bn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),bn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:bn,selector:"tb-relations-query-config-old",inputs:{disabled:"disabled",required:"required"},providers:[{provide:P,useExisting:i((()=>bn)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Pe.RelationFiltersComponent,selector:"tb-relation-filters",inputs:["disabled","allowedEntityTypes"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:bn,decorators:[{type:n,args:[{selector:"tb-relations-query-config-old",providers:[{provide:P,useExisting:i((()=>bn)),multi:!0}],template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]},propDecorators:{disabled:[{type:l}],required:[{type:l}]}});class hn{constructor(e,t,n){this.store=e,this.translate=t,this.fb=n,this.destroy$=new qe,this.separatorKeysCodes=[le,se,me]}ngOnInit(){this.attributeControlGroup=this.fb.group({clientAttributeNames:[null,[]],sharedAttributeNames:[null,[]],serverAttributeNames:[null,[]],latestTsKeyNames:[null,[]],getLatestValueWithTs:[!1,[]]}),this.attributeControlGroup.valueChanges.pipe(Le(this.destroy$)).subscribe((e=>{this.propagateChange(e)}))}writeValue(e){this.attributeControlGroup.patchValue(e,{emitEvent:!1})}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){e?this.attributeControlGroup.disable({emitEvent:!1}):this.attributeControlGroup.enable({emitEvent:!1})}ngOnDestroy(){this.destroy$.next(null),this.destroy$.complete()}removeKey(e,t){const n=this.attributeControlGroup.get(t).value,r=n.indexOf(e);r>=0&&(n.splice(r,1),this.attributeControlGroup.get(t).setValue(n,{emitEvent:!0}))}addKey(e,t){const n=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.attributeControlGroup.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.attributeControlGroup.get(t).setValue(e,{emitEvent:!0}))}n&&(n.value="")}clearChipGrid(e){this.attributeControlGroup.get(e).patchValue([],{emitEvent:!0})}}e("SelectAttributesComponent",hn),hn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:hn,deps:[{token:E.Store},{token:$.TranslateService},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),hn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:hn,selector:"tb-select-attributes",inputs:{popupHelpLink:"popupHelpLink"},providers:[{provide:P,useExisting:i((()=>hn)),multi:!0}],ngImport:t,template:'
\n \n tb.rulenode.client-attributes\n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.shared-attributes\n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.server-attributes\n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.latest-telemetry\n \n \n {{key}}\n close\n \n \n \n \n \n
\n {{ \'tb.rulenode.kv-map-pattern-hint\' | translate }}\n \n
\n \n \n
\n',styles:[":host ::ng-deep .chip-grid{width:100%;margin-bottom:16px}:host ::ng-deep .fetch-slide-toggle{width:100%;margin-bottom:22px;display:block}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:Ee.HelpPopupComponent,selector:"[tb-help-popup], [tb-help-popup-content]",inputs:["tb-help-popup","tb-help-popup-content","trigger-text","trigger-style","tb-help-popup-placement","tb-help-popup-style"]},{kind:"component",type:ae.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"directive",type:ye.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"component",type:pe.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:pe.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:pe.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:pe.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:un,selector:"tb-slide-toggle",inputs:["slideToggleName","slideToggleTooltip"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:hn,decorators:[{type:n,args:[{selector:"tb-select-attributes",providers:[{provide:P,useExisting:i((()=>hn)),multi:!0}],template:'
\n \n tb.rulenode.client-attributes\n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.shared-attributes\n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.server-attributes\n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.latest-telemetry\n \n \n {{key}}\n close\n \n \n \n \n \n
\n {{ \'tb.rulenode.kv-map-pattern-hint\' | translate }}\n \n
\n \n \n
\n',styles:[":host ::ng-deep .chip-grid{width:100%;margin-bottom:16px}:host ::ng-deep .fetch-slide-toggle{width:100%;margin-bottom:22px;display:block}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:D.FormBuilder}]},propDecorators:{popupHelpLink:[{type:l}]}});class Cn{}e("RulenodeCoreConfigCommonModule",Cn),Cn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Cn,deps:[],target:t.ɵɵFactoryTarget.NgModule}),Cn.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:Cn,declarations:[mn,pn,cn,fn,gn,Ye,rn,on,ln,Xt,yn,un,xn,dn,bn,hn],imports:[B,k,Ae],exports:[mn,pn,cn,fn,gn,Ye,rn,on,ln,Xt,yn,un,xn,dn,bn,hn]}),Cn.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Cn,imports:[B,k,Ae]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Cn,decorators:[{type:s,args:[{declarations:[mn,pn,cn,fn,gn,Ye,rn,on,ln,Xt,yn,un,xn,dn,bn,hn],imports:[B,k,Ae],exports:[mn,pn,cn,fn,gn,Ye,rn,on,ln,Xt,yn,un,xn,dn,bn,hn]}]}]});class vn{}e("RuleNodeCoreConfigActionModule",vn),vn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:vn,deps:[],target:t.ɵɵFactoryTarget.NgModule}),vn.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:vn,declarations:[nn,Xe,en,Wt,_t,We,et,tt,nt,$t,rt,at,zt,jt,Yt,Zt,tn,Ze,ot,Jt,Qt,an,sn],imports:[B,k,Ae,Cn],exports:[nn,Xe,en,Wt,_t,We,et,tt,nt,$t,rt,at,zt,jt,Yt,Zt,tn,Ze,ot,Jt,Qt,an,sn]}),vn.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:vn,imports:[B,k,Ae,Cn]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:vn,decorators:[{type:s,args:[{declarations:[nn,Xe,en,Wt,_t,We,et,tt,nt,$t,rt,at,zt,jt,Yt,Zt,tn,Ze,ot,Jt,Qt,an,sn],imports:[B,k,Ae,Cn],exports:[nn,Xe,en,Wt,_t,We,et,tt,nt,$t,rt,at,zt,jt,Yt,Zt,tn,Ze,ot,Jt,Qt,an,sn]}]}]});class Fn extends m{constructor(e,t,n){super(e),this.store=e,this.translate=t,this.fb=n,this.separatorKeysCodes=[le,se,me]}configForm(){return this.calculateDeltaConfigForm}onConfigurationSet(e){this.calculateDeltaConfigForm=this.fb.group({inputValueKey:[e.inputValueKey,[V.required,V.pattern(/(?:.|\s)*\S(&:.|\s)*/)]],outputValueKey:[e.outputValueKey,[V.required,V.pattern(/(?:.|\s)*\S(&:.|\s)*/)]],useCache:[e.useCache,[]],addPeriodBetweenMsgs:[e.addPeriodBetweenMsgs,[]],periodValueKey:[e.periodValueKey,[]],round:[e.round,[V.min(0),V.max(15)]],tellFailureIfDeltaIsNegative:[e.tellFailureIfDeltaIsNegative,[]]})}prepareInputConfig(e){return{inputValueKey:ee(e?.inputValueKey)?e.inputValueKey:null,outputValueKey:ee(e?.outputValueKey)?e.outputValueKey:null,useCache:!ee(e?.useCache)||e.useCache,addPeriodBetweenMsgs:!!ee(e?.addPeriodBetweenMsgs)&&e.addPeriodBetweenMsgs,periodValueKey:ee(e?.periodValueKey)?e.periodValueKey:null,round:ee(e?.round)?e.round:null,tellFailureIfDeltaIsNegative:!ee(e?.tellFailureIfDeltaIsNegative)||e.tellFailureIfDeltaIsNegative}}prepareOutputConfig(e){return te(e)}updateValidators(e){this.calculateDeltaConfigForm.get("addPeriodBetweenMsgs").value?this.calculateDeltaConfigForm.get("periodValueKey").setValidators([V.required]):this.calculateDeltaConfigForm.get("periodValueKey").setValidators([]),this.calculateDeltaConfigForm.get("periodValueKey").updateValueAndValidity({emitEvent:e})}validatorTriggers(){return["addPeriodBetweenMsgs"]}}e("CalculateDeltaConfigComponent",Fn),Fn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Fn,deps:[{token:E.Store},{token:$.TranslateService},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Fn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Fn,selector:"tb-enrichment-node-calculate-delta-config",usesInheritance:!0,ngImport:t,template:"
\n
\n \n {{ 'tb.rulenode.input-value-key' | translate }}\n \n \n {{ 'tb.rulenode.input-value-key-required' | translate }}\n \n \n \n {{ 'tb.rulenode.output-value-key' | translate }}\n \n \n {{ 'tb.rulenode.output-value-key-required' | translate }}\n \n \n
\n \n {{ 'tb.rulenode.number-of-digits-after-floating-point' | translate }}\n \n \n {{ 'tb.rulenode.number-of-digits-after-floating-point-range' | translate }}\n \n \n {{ 'tb.rulenode.number-of-digits-after-floating-point-range' | translate }}\n \n \n
\n \n \n \n \n \n \n
\n \n {{ 'tb.rulenode.period-value-key' | translate }}\n \n \n {{ 'tb.rulenode.period-value-key-required' | translate }}\n \n \n
\n",styles:[":host ::ng-deep .slide-toggles-block .slide-toggle{margin:12px 0}:host ::ng-deep .period-input{margin-top:20px}\n"],dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:un,selector:"tb-slide-toggle",inputs:["slideToggleName","slideToggleTooltip"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Fn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-calculate-delta-config",template:"
\n
\n \n {{ 'tb.rulenode.input-value-key' | translate }}\n \n \n {{ 'tb.rulenode.input-value-key-required' | translate }}\n \n \n \n {{ 'tb.rulenode.output-value-key' | translate }}\n \n \n {{ 'tb.rulenode.output-value-key-required' | translate }}\n \n \n
\n \n {{ 'tb.rulenode.number-of-digits-after-floating-point' | translate }}\n \n \n {{ 'tb.rulenode.number-of-digits-after-floating-point-range' | translate }}\n \n \n {{ 'tb.rulenode.number-of-digits-after-floating-point-range' | translate }}\n \n \n
\n \n \n \n \n \n \n
\n \n {{ 'tb.rulenode.period-value-key' | translate }}\n \n \n {{ 'tb.rulenode.period-value-key-required' | translate }}\n \n \n
\n",styles:[":host ::ng-deep .slide-toggles-block .slide-toggle{margin:12px 0}:host ::ng-deep .period-input{margin-top:20px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:D.FormBuilder}]}});class Ln extends m{constructor(e,t,n){super(e),this.store=e,this.fb=t,this.translate=n,this.fetchToData=[],this.DataToFetch=Ct;for(const e of vt.keys())e!==Ct.FIELDS&&this.fetchToData.push({value:e,name:this.translate.instant(vt.get(e))})}configForm(){return this.customerAttributesConfigForm}prepareOutputConfig(e){const t={};for(const n of Object.keys(e.dataMapping))t[n.trim()]=e.dataMapping[n];return e.dataMapping=t,te(e)}toggleChange(e){this.customerAttributesConfigForm.get("dataToFetch").patchValue(e,{emitEvent:!0})}prepareInputConfig(e){let t,n;return t=ee(e?.telemetry)?e.telemetry?Ct.LATEST_TELEMETRY:Ct.ATTRIBUTES:ee(e?.dataToFetch)?e.dataToFetch:Ct.ATTRIBUTES,n=ee(e?.attrMapping)?e.attrMapping:ee(e?.dataMapping)?e.dataMapping:null,{dataToFetch:t,dataMapping:n,fetchTo:ee(e?.fetchTo)?e.fetchTo:Pt.METADATA}}selectTranslation(e,t){return this.customerAttributesConfigForm.get("dataToFetch").value===Ct.LATEST_TELEMETRY?e:t}onConfigurationSet(e){this.customerAttributesConfigForm=this.fb.group({dataToFetch:[e.dataToFetch,[]],dataMapping:[e.dataMapping,[V.required]],fetchTo:[e.fetchTo]})}}e("CustomerAttributesConfigComponent",Ln),Ln.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ln,deps:[{token:E.Store},{token:D.FormBuilder},{token:$.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Ln.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Ln,selector:"tb-enrichment-node-customer-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n \n \n
\n',styles:[":host .fetch-to-data-toggle{margin-bottom:12px;width:420px}\n"],dependencies:[{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:He.ToggleHeaderComponent,selector:"tb-toggle-header",inputs:["value","name","useSelectOnMdLg","ignoreMdLgSize","appearance","disabled"],outputs:["valueChange"]},{kind:"component",type:mn,selector:"tb-kv-map-config",inputs:["disabled","uniqueKeyValuePairValidator","labelText","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","popupHelpLink","required"]},{kind:"component",type:yn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"component",type:dn,selector:"tb-fieldset-component",inputs:["label","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ln,decorators:[{type:n,args:[{selector:"tb-enrichment-node-customer-attributes-config",template:'
\n \n \n \n \n \n \n \n
\n',styles:[":host .fetch-to-data-toggle{margin-bottom:12px;width:420px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.FormBuilder},{type:$.TranslateService}]}});class kn extends m{constructor(e,t,n){super(e),this.store=e,this.translate=t,this.fb=n}configForm(){return this.deviceAttributesConfigForm}onConfigurationSet(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e.deviceRelationsQuery,[V.required]],tellFailureIfAbsent:[e.tellFailureIfAbsent,[]],fetchTo:[e.fetchTo,[]],attributesControl:[e.attributesControl,[]]})}prepareInputConfig(e){return ne(e)&&(e.attributesControl={clientAttributeNames:ee(e?.clientAttributeNames)?e.clientAttributeNames:null,latestTsKeyNames:ee(e?.latestTsKeyNames)?e.latestTsKeyNames:null,serverAttributeNames:ee(e?.serverAttributeNames)?e.serverAttributeNames:null,sharedAttributeNames:ee(e?.sharedAttributeNames)?e.sharedAttributeNames:null,getLatestValueWithTs:!!ee(e?.getLatestValueWithTs)&&e.getLatestValueWithTs}),{deviceRelationsQuery:ee(e?.deviceRelationsQuery)?e.deviceRelationsQuery:null,tellFailureIfAbsent:!ee(e?.tellFailureIfAbsent)||e.tellFailureIfAbsent,fetchTo:ee(e?.fetchTo)?e.fetchTo:Pt.METADATA,attributesControl:e?e.attributesControl:null}}prepareOutputConfig(e){for(const t of Object.keys(e.attributesControl))e[t]=e.attributesControl[t];return delete e.attributesControl,e}}e("DeviceAttributesConfigComponent",kn),kn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:kn,deps:[{token:E.Store},{token:$.TranslateService},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),kn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:kn,selector:"tb-enrichment-node-device-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n \n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}:host .device-relations{width:100%}:host .failure-toggle{margin:25px 0}:host .device-attribute{margin-top:12px}\n"],dependencies:[{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:pn,selector:"tb-device-relations-query-config",inputs:["disabled","required"]},{kind:"component",type:yn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"component",type:un,selector:"tb-slide-toggle",inputs:["slideToggleName","slideToggleTooltip"]},{kind:"component",type:dn,selector:"tb-fieldset-component",inputs:["label","required"]},{kind:"component",type:hn,selector:"tb-select-attributes",inputs:["popupHelpLink"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:kn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-device-attributes-config",template:'
\n \n \n \n \n \n \n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}:host .device-relations{width:100%}:host .failure-toggle{margin:25px 0}:host .device-attribute{margin-top:12px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:D.FormBuilder}]}});class Tn extends m{constructor(e,t,n){super(e),this.store=e,this.translate=t,this.fb=n,this.entityDetailsTranslationsMap=gt,this.entityDetailsList=[],this.searchText="",this.displayDetailsFn=this.displayDetails.bind(this);for(const e of Object.keys(ft))this.entityDetailsList.push(ft[e]);this.detailsFormControl=new O(""),this.filteredEntityDetails=this.detailsFormControl.valueChanges.pipe(ke(""),ve((e=>e||"")),Fe((e=>this.fetchEntityDetails(e))),Te())}ngOnInit(){super.ngOnInit()}configForm(){return this.entityDetailsConfigForm}prepareInputConfig(e){let t;return this.searchText="",this.detailsFormControl.patchValue("",{emitEvent:!0}),this.detailsList=e?e.detailsList:[],t=ee(e?.addToMetadata)?e.addToMetadata?Pt.METADATA:Pt.DATA:e?.fetchTo?e.fetchTo:Pt.DATA,{detailsList:ee(e?.detailsList)?e.detailsList:null,fetchTo:t}}prepareOutputConfig(e){return e.detailsList=this.detailsList,e}onConfigurationSet(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e.detailsList,[V.required]],fetchTo:[e.fetchTo,[]]}),this.detailsList=e?e.detailsList:[]}displayDetails(e){return e?this.translate.instant(gt.get(e)):void 0}fetchEntityDetails(e){if(this.searchText=e,this.searchText&&this.searchText.length){const e=this.searchText.toUpperCase();return Se(this.entityDetailsList.filter((t=>this.translate.instant(gt.get(ft[t])).toUpperCase().includes(e))))}return Se(this.entityDetailsList)}detailsFieldSelected(e){this.addDetailsField(e.option.value),this.clear("")}removeDetailsField(e){const t=this.detailsList.indexOf(e);t>=0&&(this.detailsList.splice(t,1),this.entityDetailsConfigForm.get("detailsList").setValue(this.detailsList))}addDetailsField(e){this.detailsList||(this.detailsList=[]);-1===this.detailsList.indexOf(e)&&(this.detailsList.push(e),this.entityDetailsConfigForm.get("detailsList").setValue(this.detailsList))}onEntityDetailsInputFocus(){this.detailsFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})}clearChipGrid(){this.detailsList=[],this.entityDetailsConfigForm.get("detailsList").patchValue([],{emitEvent:!0}),setTimeout((()=>{this.detailsInput.nativeElement.blur(),this.detailsInput.nativeElement.focus()}),0)}clear(e=""){this.detailsInput.nativeElement.value=e,this.detailsFormControl.patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.detailsInput.nativeElement.blur(),this.detailsInput.nativeElement.focus()}),0)}}e("EntityDetailsConfigComponent",Tn),Tn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Tn,deps:[{token:E.Store},{token:$.TranslateService},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Tn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Tn,selector:"tb-enrichment-node-entity-details-config",viewQueries:[{propertyName:"detailsInput",first:!0,predicate:["detailsInput"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.entity-details\' | translate }}\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n \n
\n
\n {{ \'tb.rulenode.no-entity-details-matching\' | translate }}\n
\n
\n
\n
\n {{ \'tb.rulenode.entity-details-list-empty\' | translate }}\n
\n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ae.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:ye.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"component",type:Ie.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple","hideSingleSelectionIndicator"],exportAs:["matAutocomplete"]},{kind:"directive",type:Ie.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{kind:"directive",type:Ie.MatAutocompleteOrigin,selector:"[matAutocompleteOrigin]",exportAs:["matAutocompleteOrigin"]},{kind:"component",type:pe.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:pe.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:pe.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:pe.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormControlDirective,selector:"[formControl]",inputs:["formControl","disabled","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:yn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"pipe",type:K.AsyncPipe,name:"async"},{kind:"pipe",type:Ne.HighlightPipe,name:"highlight"},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Tn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-entity-details-config",template:'
\n \n {{ \'tb.rulenode.entity-details\' | translate }}\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n \n
\n
\n {{ \'tb.rulenode.no-entity-details-matching\' | translate }}\n
\n
\n
\n
\n {{ \'tb.rulenode.entity-details-list-empty\' | translate }}\n
\n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:D.FormBuilder}]},propDecorators:{detailsInput:[{type:a,args:["detailsInput",{static:!1}]}]}});class In extends m{constructor(e,t,n){super(e),this.store=e,this.translate=t,this.fb=n,this.separatorKeysCodes=[le,se,me],this.aggregationTypes=T,this.aggregations=Object.keys(T),this.aggregationTypesTranslations=I,this.fetchMode=yt,this.samplingOrders=Object.keys(ht),this.samplingOrdersTranslate=Lt,this.timeUnits=Object.values(ut),this.timeUnitsTranslationMap=pt,this.deduplicationStrategiesHintTranslations=bt,this.headerOptions=[],this.timeUnitMap={[ut.MILLISECONDS]:1,[ut.SECONDS]:1e3,[ut.MINUTES]:6e4,[ut.HOURS]:36e5,[ut.DAYS]:864e5},this.intervalValidator=()=>e=>e.get("startInterval").value*this.timeUnitMap[e.get("startIntervalTimeUnit").value]<=e.get("endInterval").value*this.timeUnitMap[e.get("endIntervalTimeUnit").value]?{intervalError:!0}:null;for(const e of xt.keys())this.headerOptions.push({value:e,name:this.translate.instant(xt.get(e))})}configForm(){return this.getTelemetryFromDatabaseConfigForm}onConfigurationSet(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e.latestTsKeyNames,[]],aggregation:[e.aggregation,[V.required]],fetchMode:[e.fetchMode,[V.required]],orderBy:[e.orderBy,[]],limit:[e.limit,[]],useMetadataIntervalPatterns:[e.useMetadataIntervalPatterns,[]],interval:this.fb.group({startInterval:[e.interval.startInterval,[]],startIntervalTimeUnit:[e.interval.startIntervalTimeUnit,[]],endInterval:[e.interval.endInterval,[]],endIntervalTimeUnit:[e.interval.endIntervalTimeUnit,[]]}),startIntervalPattern:[e.startIntervalPattern,[]],endIntervalPattern:[e.endIntervalPattern,[]]})}validatorTriggers(){return["fetchMode","useMetadataIntervalPatterns"]}toggleChange(e){this.getTelemetryFromDatabaseConfigForm.get("fetchMode").patchValue(e,{emitEvent:!0})}prepareOutputConfig(e){return e.startInterval=e.interval.startInterval,e.startIntervalTimeUnit=e.interval.startIntervalTimeUnit,e.endInterval=e.interval.endInterval,e.endIntervalTimeUnit=e.interval.endIntervalTimeUnit,delete e.interval,te(e)}prepareInputConfig(e){return ne(e)&&(e.interval={startInterval:e.startInterval,startIntervalTimeUnit:e.startIntervalTimeUnit,endInterval:e.endInterval,endIntervalTimeUnit:e.endIntervalTimeUnit}),{latestTsKeyNames:ee(e?.latestTsKeyNames)?e.latestTsKeyNames:null,aggregation:ee(e?.aggregation)?e.aggregation:T.NONE,fetchMode:ee(e?.fetchMode)?e.fetchMode:yt.FIRST,orderBy:ee(e?.orderBy)?e.orderBy:ht.ASC,limit:ee(e?.limit)?e.limit:1e3,useMetadataIntervalPatterns:!!ee(e?.useMetadataIntervalPatterns)&&e.useMetadataIntervalPatterns,interval:{startInterval:ee(e?.interval?.startInterval)?e.interval.startInterval:2,startIntervalTimeUnit:ee(e?.interval?.startIntervalTimeUnit)?e.interval.startIntervalTimeUnit:ut.MINUTES,endInterval:ee(e?.interval?.endInterval)?e.interval.endInterval:1,endIntervalTimeUnit:ee(e?.interval?.endIntervalTimeUnit)?e.interval.endIntervalTimeUnit:ut.MINUTES},startIntervalPattern:ee(e?.startIntervalPattern)?e.startIntervalPattern:null,endIntervalPattern:ee(e?.endIntervalPattern)?e.endIntervalPattern:null}}updateValidators(e){const t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,n=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===yt.ALL?(this.getTelemetryFromDatabaseConfigForm.get("aggregation").setValidators([V.required]),this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([V.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([V.required,V.min(2),V.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("aggregation").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),n?(this.getTelemetryFromDatabaseConfigForm.get("interval.startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("interval.startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("interval.endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("interval.endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("interval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([V.required,V.pattern(/(?:.|\s)*\S(&:.|\s)*/)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([V.required,V.pattern(/(?:.|\s)*\S(&:.|\s)*/)])):(this.getTelemetryFromDatabaseConfigForm.get("interval.startInterval").setValidators([V.required,V.min(1),V.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("interval.startIntervalTimeUnit").setValidators([V.required]),this.getTelemetryFromDatabaseConfigForm.get("interval.endInterval").setValidators([V.required,V.min(1),V.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("interval.endIntervalTimeUnit").setValidators([V.required]),this.getTelemetryFromDatabaseConfigForm.get("interval").setValidators([this.intervalValidator()]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([])),this.getTelemetryFromDatabaseConfigForm.get("aggregation").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("orderBy").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("limit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("interval.startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("interval.startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("interval.endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("interval.endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("interval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})}removeKey(e,t){const n=this.getTelemetryFromDatabaseConfigForm.get(t).value,r=n.indexOf(e);r>=0&&(n.splice(r,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(n,{emitEvent:!0}))}clearChipGrid(){this.getTelemetryFromDatabaseConfigForm.get("latestTsKeyNames").patchValue([],{emitEvent:!0})}addKey(e,t){const n=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.getTelemetryFromDatabaseConfigForm.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(e,{emitEvent:!0}))}n&&(n.value="")}}e("GetTelemetryFromDatabaseConfigComponent",In),In.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:In,deps:[{token:E.Store},{token:$.TranslateService},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),In.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:In,selector:"tb-enrichment-node-get-telemetry-from-database",usesInheritance:!0,ngImport:t,template:'
\n \n {{\'tb.rulenode.timeseries-keys\' | translate}}\n \n \n {{key}}\n close\n \n \n \n \n \n {{ "tb.rulenode.general-pattern-hint" | translate }}\n \n \n \n \n\n \n \n
\n
\n \n {{ \'tb.rulenode.interval-start\' | translate }}\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n {{ \'tb.rulenode.time-unit\' | translate }}\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n {{ \'tb.rulenode.interval-end\' | translate }}\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n {{ \'tb.rulenode.time-unit\' | translate }}\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n {{ \'tb.rulenode.fetch-timeseries-from-to\' | translate:\n {\n startInterval: getTelemetryFromDatabaseConfigForm.get(\'interval.startInterval\').value,\n endInterval: getTelemetryFromDatabaseConfigForm.get(\'interval.endInterval\').value,\n startIntervalTimeUnit: getTelemetryFromDatabaseConfigForm.get(\'interval.startIntervalTimeUnit\').value.toLowerCase(),\n endIntervalTimeUnit: getTelemetryFromDatabaseConfigForm.get(\'interval.endIntervalTimeUnit\').value.toLowerCase()} }}\n
\n
\n {{ "tb.rulenode.fetch-timeseries-from-to-invalid" | translate }}\n
\n
\n \n
\n \n {{ \'tb.rulenode.start-interval\' | translate }}\n \n \n {{ \'tb.rulenode.start-interval-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.end-interval\' | translate }}\n \n \n {{ \'tb.rulenode.end-interval-required\' | translate }}\n \n \n
\n {{ \'tb.rulenode.metadata-dynamic-interval-hint\' | translate }}\n \n
\n
\n
\n
\n \n
\n \n \n
\n {{ deduplicationStrategiesHintTranslations.get(getTelemetryFromDatabaseConfigForm.get(\'fetchMode\').value) | translate }}\n
\n
\n \n {{ \'aggregation.function\' | translate }}\n \n \n {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }}\n \n \n \n
\n \n {{ "tb.rulenode.order-by-timestamp" | translate }} \n \n \n {{ samplingOrdersTranslate.get(order) | translate }}\n \n \n \n \n {{ "tb.rulenode.limit" | translate }}\n \n {{ "tb.rulenode.limit-hint" | translate }}\n \n {{ \'tb.rulenode.limit-required\' | translate }}\n \n \n {{ \'tb.rulenode.limit-range\' | translate }}\n \n \n {{ \'tb.rulenode.limit-range\' | translate }}\n \n \n
\n
\n
\n
\n
\n',styles:[":host ::ng-deep label.tb-title{margin-bottom:-10px}:host ::ng-deep .fetch-interval{margin-top:12px}:host ::ng-deep .fetch-interval .interval-slide-toggle{width:100%;margin:4px 0 16px}:host ::ng-deep .fetch-interval .input-block{width:100%}:host ::ng-deep .interval-description{text-align:center;font-size:12px;color:#3d3d3d;margin-bottom:9px;font-weight:500}:host ::ng-deep .fetch-strategy-fieldset{margin-top:12px}:host ::ng-deep .fetch-strategy-fieldset .fetch-strategy-block{margin-top:8px;align-items:center;width:100%}:host ::ng-deep .fetch-strategy-fieldset .fetch-strategy-block .fetch-mod-toggle{margin-bottom:12px;width:630px}:host ::ng-deep .fetch-strategy-fieldset .fetch-strategy-block .input-block{width:100%}:host ::ng-deep .fetch-strategy-fieldset .fetch-strategy-block .input-block .additional-inputs{margin-bottom:16px}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:Ee.HelpPopupComponent,selector:"[tb-help-popup], [tb-help-popup-content]",inputs:["tb-help-popup","tb-help-popup-content","trigger-text","trigger-style","tb-help-popup-placement","tb-help-popup-style"]},{kind:"component",type:ae.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:ye.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"component",type:pe.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:pe.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:pe.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:pe.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:D.FormGroupName,selector:"[formGroupName]",inputs:["formGroupName"]},{kind:"component",type:He.ToggleHeaderComponent,selector:"tb-toggle-header",inputs:["value","name","useSelectOnMdLg","ignoreMdLgSize","appearance","disabled"],outputs:["valueChange"]},{kind:"component",type:un,selector:"tb-slide-toggle",inputs:["slideToggleName","slideToggleTooltip"]},{kind:"component",type:dn,selector:"tb-fieldset-component",inputs:["label","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:In,decorators:[{type:n,args:[{selector:"tb-enrichment-node-get-telemetry-from-database",template:'
\n \n {{\'tb.rulenode.timeseries-keys\' | translate}}\n \n \n {{key}}\n close\n \n \n \n \n \n {{ "tb.rulenode.general-pattern-hint" | translate }}\n \n \n \n \n\n \n \n
\n
\n \n {{ \'tb.rulenode.interval-start\' | translate }}\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n {{ \'tb.rulenode.time-unit\' | translate }}\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n {{ \'tb.rulenode.interval-end\' | translate }}\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n {{ \'tb.rulenode.time-unit\' | translate }}\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n {{ \'tb.rulenode.fetch-timeseries-from-to\' | translate:\n {\n startInterval: getTelemetryFromDatabaseConfigForm.get(\'interval.startInterval\').value,\n endInterval: getTelemetryFromDatabaseConfigForm.get(\'interval.endInterval\').value,\n startIntervalTimeUnit: getTelemetryFromDatabaseConfigForm.get(\'interval.startIntervalTimeUnit\').value.toLowerCase(),\n endIntervalTimeUnit: getTelemetryFromDatabaseConfigForm.get(\'interval.endIntervalTimeUnit\').value.toLowerCase()} }}\n
\n
\n {{ "tb.rulenode.fetch-timeseries-from-to-invalid" | translate }}\n
\n
\n \n
\n \n {{ \'tb.rulenode.start-interval\' | translate }}\n \n \n {{ \'tb.rulenode.start-interval-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.end-interval\' | translate }}\n \n \n {{ \'tb.rulenode.end-interval-required\' | translate }}\n \n \n
\n {{ \'tb.rulenode.metadata-dynamic-interval-hint\' | translate }}\n \n
\n
\n
\n
\n \n
\n \n \n
\n {{ deduplicationStrategiesHintTranslations.get(getTelemetryFromDatabaseConfigForm.get(\'fetchMode\').value) | translate }}\n
\n
\n \n {{ \'aggregation.function\' | translate }}\n \n \n {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }}\n \n \n \n
\n \n {{ "tb.rulenode.order-by-timestamp" | translate }} \n \n \n {{ samplingOrdersTranslate.get(order) | translate }}\n \n \n \n \n {{ "tb.rulenode.limit" | translate }}\n \n {{ "tb.rulenode.limit-hint" | translate }}\n \n {{ \'tb.rulenode.limit-required\' | translate }}\n \n \n {{ \'tb.rulenode.limit-range\' | translate }}\n \n \n {{ \'tb.rulenode.limit-range\' | translate }}\n \n \n
\n
\n
\n
\n
\n',styles:[":host ::ng-deep label.tb-title{margin-bottom:-10px}:host ::ng-deep .fetch-interval{margin-top:12px}:host ::ng-deep .fetch-interval .interval-slide-toggle{width:100%;margin:4px 0 16px}:host ::ng-deep .fetch-interval .input-block{width:100%}:host ::ng-deep .interval-description{text-align:center;font-size:12px;color:#3d3d3d;margin-bottom:9px;font-weight:500}:host ::ng-deep .fetch-strategy-fieldset{margin-top:12px}:host ::ng-deep .fetch-strategy-fieldset .fetch-strategy-block{margin-top:8px;align-items:center;width:100%}:host ::ng-deep .fetch-strategy-fieldset .fetch-strategy-block .fetch-mod-toggle{margin-bottom:12px;width:630px}:host ::ng-deep .fetch-strategy-fieldset .fetch-strategy-block .input-block{width:100%}:host ::ng-deep .fetch-strategy-fieldset .fetch-strategy-block .input-block .additional-inputs{margin-bottom:16px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:D.FormBuilder}]}});class Nn extends m{constructor(e,t,n){super(e),this.store=e,this.translate=t,this.fb=n}configForm(){return this.originatorAttributesConfigForm}onConfigurationSet(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[e.tellFailureIfAbsent,[]],fetchTo:[e.fetchTo,[]],attributesControl:[e.attributesControl,[]]})}prepareInputConfig(e){return ne(e)&&(e.attributesControl={clientAttributeNames:ee(e?.clientAttributeNames)?e.clientAttributeNames:null,latestTsKeyNames:ee(e?.latestTsKeyNames)?e.latestTsKeyNames:null,serverAttributeNames:ee(e?.serverAttributeNames)?e.serverAttributeNames:null,sharedAttributeNames:ee(e?.sharedAttributeNames)?e.sharedAttributeNames:null,getLatestValueWithTs:!!ee(e?.getLatestValueWithTs)&&e.getLatestValueWithTs}),{fetchTo:ee(e?.fetchTo)?e.fetchTo:Pt.METADATA,tellFailureIfAbsent:!!ee(e?.tellFailureIfAbsent)&&e.tellFailureIfAbsent,attributesControl:ee(e?.attributesControl)?e.attributesControl:null}}prepareOutputConfig(e){for(const t of Object.keys(e.attributesControl))e[t]=e.attributesControl[t];return delete e.attributesControl,e}}e("OriginatorAttributesConfigComponent",Nn),Nn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Nn,deps:[{token:E.Store},{token:$.TranslateService},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Nn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Nn,selector:"tb-enrichment-node-originator-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}:host .failure-slide-toggle{margin:25px 0}\n"],dependencies:[{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:yn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"component",type:un,selector:"tb-slide-toggle",inputs:["slideToggleName","slideToggleTooltip"]},{kind:"component",type:dn,selector:"tb-fieldset-component",inputs:["label","required"]},{kind:"component",type:hn,selector:"tb-select-attributes",inputs:["popupHelpLink"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Nn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-originator-attributes-config",template:'
\n \n \n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}:host .failure-slide-toggle{margin:25px 0}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:D.FormBuilder}]}});class Sn extends m{constructor(e,t,n){super(e),this.store=e,this.fb=t,this.translate=n,this.originatorFields=[];for(const e of Object.keys(N))this.originatorFields.push({value:N[e].value,name:this.translate.instant(N[e].name)})}configForm(){return this.originatorFieldsConfigForm}prepareOutputConfig(e){return te(e)}prepareInputConfig(e){return{dataMapping:ee(e?.dataMapping)?e.dataMapping:null,ignoreNullStrings:ee(e?.ignoreNullStrings)?e.ignoreNullStrings:null,fetchTo:ee(e?.fetchTo)?e.fetchTo:Pt.METADATA}}onConfigurationSet(e){this.originatorFieldsConfigForm=this.fb.group({dataMapping:[e.dataMapping,[V.required]],ignoreNullStrings:[e.ignoreNullStrings,[]],fetchTo:[e.fetchTo,[]]})}}e("OriginatorFieldsConfigComponent",Sn),Sn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Sn,deps:[{token:E.Store},{token:D.FormBuilder},{token:$.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Sn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Sn,selector:"tb-enrichment-node-originator-fields-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n \n \n
\n',styles:[":host .msg-metadata-chip{margin-bottom:12px}:host .skip-slide-toggle{margin-top:20px}\n"],dependencies:[{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:yn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"component",type:un,selector:"tb-slide-toggle",inputs:["slideToggleName","slideToggleTooltip"]},{kind:"component",type:xn,selector:"tb-sv-map-config",inputs:["selectOptions","disabled","labelText","requiredText","targetKeyPrefix","selectText","selectRequiredText","valText","valRequiredText","hintText","popupHelpLink","required"]},{kind:"component",type:dn,selector:"tb-fieldset-component",inputs:["label","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Sn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-originator-fields-config",template:'
\n \n \n \n \n \n \n \n
\n',styles:[":host .msg-metadata-chip{margin-bottom:12px}:host .skip-slide-toggle{margin-top:20px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.FormBuilder},{type:$.TranslateService}]}});class qn extends m{constructor(e,t,n){super(e),this.store=e,this.fb=t,this.translate=n,this.DataToFetch=Ct,this.msgMetadataLabelTranslations=Ft,this.originatorFields=[],this.fetchToData=[],this.destroy$=new qe,this.defaultKvMap={serialNumber:"sn"},this.defaultSvMap={[N.name.value]:`relatedEntity${this.translate.instant(N.name.name)}`},this.dataToFetchPrevValue="";for(const e of Object.keys(N))this.originatorFields.push({value:N[e].value,name:this.translate.instant(N[e].name)});for(const e of vt.keys())this.fetchToData.push({value:e,name:this.translate.instant(vt.get(e))})}toggleChange(e){this.relatedAttributesConfigForm.get("dataToFetch").patchValue(e,{emitEvent:!0})}configForm(){return this.relatedAttributesConfigForm}prepareOutputConfig(e){const t={};for(const n of Object.keys(e.dataMapping))t[n.trim()]=e.dataMapping[n];return e.dataMapping=t,te(e)}prepareInputConfig(e){let t;return ee(e?.telemetry)?this.dataToFetchPrevValue=e.telemetry?Ct.LATEST_TELEMETRY:Ct.ATTRIBUTES:this.dataToFetchPrevValue=ee(e?.dataToFetch)?e.dataToFetch:Ct.ATTRIBUTES,t=ee(e?.attrMapping)?e.attrMapping:ee(e?.dataMapping)?e.dataMapping:null,{relationsQuery:ee(e?.relationsQuery)?e.relationsQuery:null,dataToFetch:this.dataToFetchPrevValue,dataMapping:t,fetchTo:ee(e?.fetchTo)?e.fetchTo:Pt.METADATA}}selectTranslation(e,t){return this.relatedAttributesConfigForm.get("dataToFetch").value===Ct.LATEST_TELEMETRY?e:t}onConfigurationSet(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e.relationsQuery,[V.required]],dataToFetch:[e.dataToFetch,[]],dataMapping:[e.dataMapping,[V.required]],fetchTo:[e.fetchTo,[]]}),this.relatedAttributesConfigForm.get("dataToFetch").valueChanges.pipe(Le(this.destroy$)).subscribe((e=>{e===Ct.FIELDS&&this.relatedAttributesConfigForm.get("dataMapping").patchValue(this.defaultSvMap,{emitEvent:!1}),e!==Ct.FIELDS&&this.dataToFetchPrevValue===Ct.FIELDS&&this.relatedAttributesConfigForm.get("dataMapping").patchValue(this.defaultKvMap,{emitEvent:!1}),this.dataToFetchPrevValue=e}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}}e("RelatedAttributesConfigComponent",qn),qn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:qn,deps:[{token:E.Store},{token:D.FormBuilder},{token:$.TranslateService}],target:t.ɵɵFactoryTarget.Component}),qn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:qn,selector:"tb-enrichment-node-related-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n',styles:[":host .fetch-data-block{margin-top:12px}:host .fetch-data-block .fetch-to-data-toggle{margin-bottom:12px;width:630px}:host .fetch-data-block .msg-metadata-chip{margin-bottom:12px}\n"],dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:He.ToggleHeaderComponent,selector:"tb-toggle-header",inputs:["value","name","useSelectOnMdLg","ignoreMdLgSize","appearance","disabled"],outputs:["valueChange"]},{kind:"component",type:mn,selector:"tb-kv-map-config",inputs:["disabled","uniqueKeyValuePairValidator","labelText","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","popupHelpLink","required"]},{kind:"component",type:cn,selector:"tb-relations-query-config",inputs:["disabled","required"]},{kind:"component",type:yn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"component",type:xn,selector:"tb-sv-map-config",inputs:["selectOptions","disabled","labelText","requiredText","targetKeyPrefix","selectText","selectRequiredText","valText","valRequiredText","hintText","popupHelpLink","required"]},{kind:"component",type:dn,selector:"tb-fieldset-component",inputs:["label","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:qn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-related-attributes-config",template:'
\n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n',styles:[":host .fetch-data-block{margin-top:12px}:host .fetch-data-block .fetch-to-data-toggle{margin-bottom:12px;width:630px}:host .fetch-data-block .msg-metadata-chip{margin-bottom:12px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.FormBuilder},{type:$.TranslateService}]}});class Mn extends m{constructor(e,t,n){super(e),this.store=e,this.fb=t,this.translate=n,this.fetchToData=[],this.DataToFetch=Ct;for(const e of vt.keys())e!==Ct.FIELDS&&this.fetchToData.push({value:e,name:this.translate.instant(vt.get(e))})}configForm(){return this.tenantAttributesConfigForm}toggleChange(e){this.tenantAttributesConfigForm.get("dataToFetch").patchValue(e,{emitEvent:!0})}prepareInputConfig(e){let t,n;return t=ee(e?.telemetry)?e.telemetry?Ct.LATEST_TELEMETRY:Ct.ATTRIBUTES:ee(e?.dataToFetch)?e.dataToFetch:Ct.ATTRIBUTES,n=ee(e?.attrMapping)?e.attrMapping:ee(e?.dataMapping)?e.dataMapping:null,{dataToFetch:t,dataMapping:n,fetchTo:ee(e?.fetchTo)?e.fetchTo:Pt.METADATA}}selectTranslation(e,t){return this.tenantAttributesConfigForm.get("dataToFetch").value===Ct.LATEST_TELEMETRY?e:t}onConfigurationSet(e){this.tenantAttributesConfigForm=this.fb.group({dataToFetch:[e.dataToFetch,[]],dataMapping:[e.dataMapping,[V.required]],fetchTo:[e.fetchTo,[]]})}}e("TenantAttributesConfigComponent",Mn),Mn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Mn,deps:[{token:E.Store},{token:D.FormBuilder},{token:$.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Mn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Mn,selector:"tb-enrichment-node-tenant-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n \n \n
\n',styles:[":host .fetch-to-data-toggle{margin-bottom:12px;width:420px}:host .msg-metadata-chip{margin-bottom:12px}\n"],dependencies:[{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:He.ToggleHeaderComponent,selector:"tb-toggle-header",inputs:["value","name","useSelectOnMdLg","ignoreMdLgSize","appearance","disabled"],outputs:["valueChange"]},{kind:"component",type:mn,selector:"tb-kv-map-config",inputs:["disabled","uniqueKeyValuePairValidator","labelText","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","popupHelpLink","required"]},{kind:"component",type:yn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"component",type:dn,selector:"tb-fieldset-component",inputs:["label","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Mn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-tenant-attributes-config",template:'
\n \n \n \n \n \n \n \n
\n',styles:[":host .fetch-to-data-toggle{margin-bottom:12px;width:420px}:host .msg-metadata-chip{margin-bottom:12px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.FormBuilder},{type:$.TranslateService}]}});class An extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.fetchDeviceCredentialsConfigForm}prepareInputConfig(e){return{fetchTo:ee(e?.fetchTo)?e.fetchTo:Pt.METADATA}}onConfigurationSet(e){this.fetchDeviceCredentialsConfigForm=this.fb.group({fetchTo:[e.fetchTo,[]]})}}e("FetchDeviceCredentialsConfigComponent",An),An.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:An,deps:[{token:E.Store},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),An.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:An,selector:"./tb-enrichment-node-fetch-device-credentials-config",usesInheritance:!0,ngImport:t,template:'
\n \n
\n',dependencies:[{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:yn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:An,decorators:[{type:n,args:[{selector:"./tb-enrichment-node-fetch-device-credentials-config",template:'
\n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.FormBuilder}]}});class Gn{}e("RulenodeCoreConfigEnrichmentModule",Gn),Gn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Gn,deps:[],target:t.ɵɵFactoryTarget.NgModule}),Gn.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:Gn,declarations:[Ln,Tn,kn,Nn,Sn,In,qn,Mn,Fn,An],imports:[B,k,Cn],exports:[Ln,Tn,kn,Nn,Sn,In,qn,Mn,Fn,An]}),Gn.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Gn,imports:[B,k,Cn]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Gn,decorators:[{type:s,args:[{declarations:[Ln,Tn,kn,Nn,Sn,In,qn,Mn,Fn,An],imports:[B,k,Cn],exports:[Ln,Tn,kn,Nn,Sn,In,qn,Mn,Fn,An]}]}]});class En extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.allAzureIotHubCredentialsTypes=St,this.azureIotHubCredentialsTypeTranslationsMap=qt}configForm(){return this.azureIotHubConfigForm}onConfigurationSet(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[V.required]],host:[e?e.host:null,[V.required]],port:[e?e.port:null,[V.required,V.min(1),V.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[V.required,V.min(1),V.max(200)]],clientId:[e?e.clientId:null,[V.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[V.required]],sasKey:[e&&e.credentials?e.credentials.sasKey:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]]})})}prepareOutputConfig(e){const t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e}validatorTriggers(){return["credentials.type"]}updateValidators(e){const t=this.azureIotHubConfigForm.get("credentials"),n=t.get("type").value;switch(e&&t.reset({type:n},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),n){case"sas":t.get("sasKey").setValidators([V.required]);break;case"cert.PEM":t.get("privateKey").setValidators([V.required]),t.get("privateKeyFileName").setValidators([V.required]),t.get("cert").setValidators([V.required]),t.get("certFileName").setValidators([V.required])}t.get("sasKey").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})}}e("AzureIotHubConfigComponent",En),En.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:En,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),En.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:En,selector:"tb-external-node-azure-iot-hub-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n \n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.NgSwitch,selector:"[ngSwitch]",inputs:["ngSwitch"]},{kind:"directive",type:K.NgSwitchCase,selector:"[ngSwitchCase]",inputs:["ngSwitchCase"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:Me.MatAccordion,selector:"mat-accordion",inputs:["multi","hideToggle","displayMode","togglePosition"],exportAs:["matAccordion"]},{kind:"component",type:Me.MatExpansionPanel,selector:"mat-expansion-panel",inputs:["disabled","expanded","hideToggle","togglePosition"],outputs:["opened","closed","expandedChange","afterExpand","afterCollapse"],exportAs:["matExpansionPanel"]},{kind:"component",type:Me.MatExpansionPanelHeader,selector:"mat-expansion-panel-header",inputs:["tabIndex","expandedHeight","collapsedHeight"]},{kind:"directive",type:Me.MatExpansionPanelTitle,selector:"mat-panel-title"},{kind:"directive",type:Me.MatExpansionPanelDescription,selector:"mat-panel-description"},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:D.FormGroupName,selector:"[formGroupName]",inputs:["formGroupName"]},{kind:"component",type:Re.FileInputComponent,selector:"tb-file-input",inputs:["label","accept","noFileText","inputId","allowedExtensions","dropLabel","contentConvertFunction","required","requiredAsError","disabled","existingFileName","readAsBinary","workFromFileObj","multipleFile"],outputs:["fileNameChanged"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Oe.TogglePasswordComponent,selector:"tb-toggle-password"},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:En,decorators:[{type:n,args:[{selector:"tb-external-node-azure-iot-hub-config",template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n \n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Dn extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.ackValues=["all","-1","0","1"],this.ToByteStandartCharsetTypesValues=At,this.ToByteStandartCharsetTypeTranslationMap=Gt}configForm(){return this.kafkaConfigForm}onConfigurationSet(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[V.required]],keyPattern:[e?e.keyPattern:null],bootstrapServers:[e?e.bootstrapServers:null,[V.required]],retries:[e?e.retries:null,[V.min(0)]],batchSize:[e?e.batchSize:null,[V.min(0)]],linger:[e?e.linger:null,[V.min(0)]],bufferMemory:[e?e.bufferMemory:null,[V.min(0)]],acks:[e?e.acks:null,[V.required]],keySerializer:[e?e.keySerializer:null,[V.required]],valueSerializer:[e?e.valueSerializer:null,[V.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})}validatorTriggers(){return["addMetadataKeyValuesAsKafkaHeaders"]}updateValidators(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([V.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})}}e("KafkaConfigComponent",Dn),Dn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Dn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Dn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Dn,selector:"tb-external-node-kafka-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.key-pattern\n \n \n \n
tb.rulenode.key-pattern-hint
\n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Dn,decorators:[{type:n,args:[{selector:"tb-external-node-kafka-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.key-pattern\n \n \n \n
tb.rulenode.key-pattern-hint
\n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Vn extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.subscriptions=[]}configForm(){return this.mqttConfigForm}onConfigurationSet(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[V.required]],host:[e?e.host:null,[V.required]],port:[e?e.port:null,[V.required,V.min(1),V.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[V.required,V.min(1),V.max(200)]],clientId:[e?e.clientId:null,[]],appendClientIdSuffix:[{value:!!e&&e.appendClientIdSuffix,disabled:!(e&&re(e.clientId))},[]],cleanSession:[!!e&&e.cleanSession,[]],retainedMessage:[!!e&&e.retainedMessage,[]],ssl:[!!e&&e.ssl,[]],credentials:[e?e.credentials:null,[]]}),this.subscriptions.push(this.mqttConfigForm.get("clientId").valueChanges.subscribe((e=>{re(e)?this.mqttConfigForm.get("appendClientIdSuffix").enable({emitEvent:!1}):this.mqttConfigForm.get("appendClientIdSuffix").disable({emitEvent:!1})})))}ngOnDestroy(){this.subscriptions.forEach((e=>e.unsubscribe()))}}e("MqttConfigComponent",Vn),Vn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Vn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Vn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Vn,selector:"tb-external-node-mqtt-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n {{\'tb.rulenode.client-id-hint\' | translate}}\n \n \n {{ \'tb.rulenode.append-client-id-suffix\' | translate }}\n \n
{{ "tb.rulenode.client-id-suffix-hint" | translate }}
\n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ "tb.rulenode.retained-message" | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}\n"],dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:gn,selector:"tb-credentials-config",inputs:["required","disableCertPemCredentials","passwordFieldRequired"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Vn,decorators:[{type:n,args:[{selector:"tb-external-node-mqtt-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n {{\'tb.rulenode.client-id-hint\' | translate}}\n \n \n {{ \'tb.rulenode.append-client-id-suffix\' | translate }}\n \n
{{ "tb.rulenode.client-id-suffix-hint" | translate }}
\n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ "tb.rulenode.retained-message" | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class wn extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.notificationType=S,this.entityType=b}configForm(){return this.notificationConfigForm}onConfigurationSet(e){this.notificationConfigForm=this.fb.group({templateId:[e?e.templateId:null,[V.required]],targets:[e?e.targets:[],[V.required]]})}}e("NotificationConfigComponent",wn),wn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:wn,deps:[{token:E.Store},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),wn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:wn,selector:"tb-external-node-notification-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n
\n',dependencies:[{kind:"component",type:Ke.EntityListComponent,selector:"tb-entity-list",inputs:["entityType","subType","labelText","placeholderText","requiredText","required","disabled","subscriptSizing","hint"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:Be.TemplateAutocompleteComponent,selector:"tb-template-autocomplete",inputs:["required","allowCreate","allowEdit","disabled","notificationTypes"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:wn,decorators:[{type:n,args:[{selector:"tb-external-node-notification-config",template:'
\n \n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.FormBuilder}]}});class Pn extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.pubSubConfigForm}onConfigurationSet(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[V.required]],topicName:[e?e.topicName:null,[V.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[V.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[V.required]],messageAttributes:[e?e.messageAttributes:null,[]]})}}e("PubSubConfigComponent",Pn),Pn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Pn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Pn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Pn,selector:"tb-external-node-pub-sub-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:Re.FileInputComponent,selector:"tb-file-input",inputs:["label","accept","noFileText","inputId","allowedExtensions","dropLabel","contentConvertFunction","required","requiredAsError","disabled","existingFileName","readAsBinary","workFromFileObj","multipleFile"],outputs:["fileNameChanged"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Pn,decorators:[{type:n,args:[{selector:"tb-external-node-pub-sub-config",template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Rn extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"]}configForm(){return this.rabbitMqConfigForm}onConfigurationSet(e){this.rabbitMqConfigForm=this.fb.group({exchangeNamePattern:[e?e.exchangeNamePattern:null,[]],routingKeyPattern:[e?e.routingKeyPattern:null,[]],messageProperties:[e?e.messageProperties:null,[]],host:[e?e.host:null,[V.required]],port:[e?e.port:null,[V.required,V.min(1),V.max(65535)]],virtualHost:[e?e.virtualHost:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]],automaticRecoveryEnabled:[!!e&&e.automaticRecoveryEnabled,[]],connectionTimeout:[e?e.connectionTimeout:null,[V.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[V.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})}}e("RabbitMqConfigComponent",Rn),Rn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Rn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Rn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Rn,selector:"tb-external-node-rabbit-mq-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Oe.TogglePasswordComponent,selector:"tb-toggle-password"},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Rn,decorators:[{type:n,args:[{selector:"tb-external-node-rabbit-mq-config",template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class On extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.proxySchemes=["http","https"],this.httpRequestTypes=Object.keys(Mt)}configForm(){return this.restApiCallConfigForm}onConfigurationSet(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[V.required]],requestMethod:[e?e.requestMethod:null,[V.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],trimDoubleQuotes:[!!e&&e.trimDoubleQuotes,[]],ignoreRequestBody:[!!e&&e.ignoreRequestBody,[]],enableProxy:[!!e&&e.enableProxy,[]],useSystemProxyProperties:[!!e&&e.enableProxy,[]],proxyScheme:[e?e.proxyHost:null,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],readTimeoutMs:[e?e.readTimeoutMs:null,[]],maxParallelRequestsCount:[e?e.maxParallelRequestsCount:null,[V.min(0)]],headers:[e?e.headers:null,[]],useRedisQueueForMsgPersistence:[!!e&&e.useRedisQueueForMsgPersistence,[]],trimQueue:[!!e&&e.trimQueue,[]],maxQueueSize:[e?e.maxQueueSize:null,[]],credentials:[e?e.credentials:null,[]]})}validatorTriggers(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]}updateValidators(e){const t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,n=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,r=this.restApiCallConfigForm.get("enableProxy").value,o=this.restApiCallConfigForm.get("useSystemProxyProperties").value;r&&!o?(this.restApiCallConfigForm.get("proxyHost").setValidators(r?[V.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(r?[V.required,V.min(1),V.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([V.min(0)])),n?this.restApiCallConfigForm.get("maxQueueSize").setValidators([V.min(0)]):this.restApiCallConfigForm.get("maxQueueSize").setValidators([]),this.restApiCallConfigForm.get("readTimeoutMs").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("maxQueueSize").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("credentials").updateValueAndValidity({emitEvent:e})}}e("RestApiCallConfigComponent",On),On.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:On,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),On.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:On,selector:"tb-external-node-rest-api-call-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n \n {{ \'tb.rulenode.trim-double-quotes\' | translate }}\n \n
tb.rulenode.trim-double-quotes-hint
\n \n {{ \'tb.rulenode.ignore-request-body\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n tb.rulenode.read-timeout-hint\n \n \n tb.rulenode.max-parallel-requests-count\n \n tb.rulenode.max-parallel-requests-count-hint\n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:gn,selector:"tb-credentials-config",inputs:["required","disableCertPemCredentials","passwordFieldRequired"]},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:On,decorators:[{type:n,args:[{selector:"tb-external-node-rest-api-call-config",template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n \n {{ \'tb.rulenode.trim-double-quotes\' | translate }}\n \n
tb.rulenode.trim-double-quotes-hint
\n \n {{ \'tb.rulenode.ignore-request-body\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n tb.rulenode.read-timeout-hint\n \n \n tb.rulenode.max-parallel-requests-count\n \n tb.rulenode.max-parallel-requests-count-hint\n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Hn extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.smtpProtocols=["smtp","smtps"],this.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"]}configForm(){return this.sendEmailConfigForm}onConfigurationSet(e){this.sendEmailConfigForm=this.fb.group({useSystemSmtpSettings:[!!e&&e.useSystemSmtpSettings,[]],smtpProtocol:[e?e.smtpProtocol:null,[]],smtpHost:[e?e.smtpHost:null,[]],smtpPort:[e?e.smtpPort:null,[]],timeout:[e?e.timeout:null,[]],enableTls:[!!e&&e.enableTls,[]],tlsVersion:[e?e.tlsVersion:null,[]],enableProxy:[!!e&&e.enableProxy,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]]})}validatorTriggers(){return["useSystemSmtpSettings","enableProxy"]}updateValidators(e){const t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,n=this.sendEmailConfigForm.get("enableProxy").value;t?(this.sendEmailConfigForm.get("smtpProtocol").setValidators([]),this.sendEmailConfigForm.get("smtpHost").setValidators([]),this.sendEmailConfigForm.get("smtpPort").setValidators([]),this.sendEmailConfigForm.get("timeout").setValidators([]),this.sendEmailConfigForm.get("proxyHost").setValidators([]),this.sendEmailConfigForm.get("proxyPort").setValidators([])):(this.sendEmailConfigForm.get("smtpProtocol").setValidators([V.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([V.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([V.required,V.min(1),V.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([V.required,V.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(n?[V.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(n?[V.required,V.min(1),V.max(65535)]:[])),this.sendEmailConfigForm.get("smtpProtocol").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpPort").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("timeout").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})}}e("SendEmailConfigComponent",Hn),Hn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Hn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Hn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Hn,selector:"tb-external-node-send-email-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n
\n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:Ue.TbCheckboxComponent,selector:"tb-checkbox",inputs:["disabled","trueValue","falseValue"],outputs:["valueChange"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Oe.TogglePasswordComponent,selector:"tb-toggle-password"},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Hn,decorators:[{type:n,args:[{selector:"tb-external-node-send-email-config",template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Kn extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.sendSmsConfigForm}onConfigurationSet(e){this.sendSmsConfigForm=this.fb.group({numbersToTemplate:[e?e.numbersToTemplate:null,[V.required]],smsMessageTemplate:[e?e.smsMessageTemplate:null,[V.required]],useSystemSmsSettings:[!!e&&e.useSystemSmsSettings,[]],smsProviderConfiguration:[e?e.smsProviderConfiguration:null,[]]})}validatorTriggers(){return["useSystemSmsSettings"]}updateValidators(e){this.sendSmsConfigForm.get("useSystemSmsSettings").value?this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([]):this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([V.required]),this.sendSmsConfigForm.get("smsProviderConfiguration").updateValueAndValidity({emitEvent:e})}}e("SendSmsConfigComponent",Kn),Kn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Kn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Kn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Kn,selector:"tb-external-node-send-sms-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.numbers-to-template\n \n \n {{ \'tb.rulenode.numbers-to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.sms-message-template\n \n \n {{ \'tb.rulenode.sms-message-template-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-system-sms-settings\' | translate }}\n \n \n \n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:ze.SmsProviderConfigurationComponent,selector:"tb-sms-provider-configuration",inputs:["required","disabled"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Kn,decorators:[{type:n,args:[{selector:"tb-external-node-send-sms-config",template:'
\n \n tb.rulenode.numbers-to-template\n \n \n {{ \'tb.rulenode.numbers-to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.sms-message-template\n \n \n {{ \'tb.rulenode.sms-message-template-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-system-sms-settings\' | translate }}\n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Bn extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.slackChanelTypes=Object.keys(q),this.slackChanelTypesTranslateMap=M}configForm(){return this.slackConfigForm}onConfigurationSet(e){this.slackConfigForm=this.fb.group({botToken:[e?e.botToken:null],useSystemSettings:[!!e&&e.useSystemSettings],messageTemplate:[e?e.messageTemplate:null,[V.required]],conversationType:[e?e.conversationType:null,[V.required]],conversation:[e?e.conversation:null,[V.required]]})}validatorTriggers(){return["useSystemSettings"]}updateValidators(e){this.slackConfigForm.get("useSystemSettings").value?this.slackConfigForm.get("botToken").clearValidators():this.slackConfigForm.get("botToken").setValidators([V.required]),this.slackConfigForm.get("botToken").updateValueAndValidity({emitEvent:e})}}e("SlackConfigComponent",Bn),Bn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Bn,deps:[{token:E.Store},{token:D.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Bn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Bn,selector:"tb-external-node-slack-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.message-template\n \n \n {{ \'tb.rulenode.message-template-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-system-slack-settings\' | translate }}\n \n \n tb.rulenode.slack-api-token\n \n \n {{ \'tb.rulenode.slack-api-token-required\' | translate }}\n \n \n \n \n \n {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }}\n \n \n \n \n
\n',styles:[":host .tb-title{display:block;padding-bottom:6px}:host ::ng-deep .mat-mdc-radio-group{display:flex;flex-direction:row;margin-bottom:22px;gap:12px}:host ::ng-deep .mat-mdc-radio-group .mat-mdc-radio-button{flex:1 1 100%;padding:4px;border:1px solid rgba(0,0,0,.12);border-radius:6px}@media screen and (max-width: 599px){:host ::ng-deep .mat-mdc-radio-group{flex-direction:column}}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_e.MatRadioGroup,selector:"mat-radio-group",exportAs:["matRadioGroup"]},{kind:"component",type:_e.MatRadioButton,selector:"mat-radio-button",inputs:["disableRipple","tabIndex"],exportAs:["matRadioButton"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:je.SlackConversationAutocompleteComponent,selector:"tb-slack-conversation-autocomplete",inputs:["labelText","requiredText","required","disabled","slackChanelType","token"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Bn,decorators:[{type:n,args:[{selector:"tb-external-node-slack-config",template:'
\n \n tb.rulenode.message-template\n \n \n {{ \'tb.rulenode.message-template-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-system-slack-settings\' | translate }}\n \n \n tb.rulenode.slack-api-token\n \n \n {{ \'tb.rulenode.slack-api-token-required\' | translate }}\n \n \n \n \n \n {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }}\n \n \n \n \n
\n',styles:[":host .tb-title{display:block;padding-bottom:6px}:host ::ng-deep .mat-mdc-radio-group{display:flex;flex-direction:row;margin-bottom:22px;gap:12px}:host ::ng-deep .mat-mdc-radio-group .mat-mdc-radio-button{flex:1 1 100%;padding:4px;border:1px solid rgba(0,0,0,.12);border-radius:6px}@media screen and (max-width: 599px){:host ::ng-deep .mat-mdc-radio-group{flex-direction:column}}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.FormBuilder}]}});class Un extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.snsConfigForm}onConfigurationSet(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[V.required]],accessKeyId:[e?e.accessKeyId:null,[V.required]],secretAccessKey:[e?e.secretAccessKey:null,[V.required]],region:[e?e.region:null,[V.required]]})}}e("SnsConfigComponent",Un),Un.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Un,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Un.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Un,selector:"tb-external-node-sns-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Un,decorators:[{type:n,args:[{selector:"tb-external-node-sns-config",template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class zn extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.sqsQueueType=kt,this.sqsQueueTypes=Object.keys(kt),this.sqsQueueTypeTranslationsMap=Tt}configForm(){return this.sqsConfigForm}onConfigurationSet(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[V.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[V.required]],delaySeconds:[e?e.delaySeconds:null,[V.min(0),V.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[V.required]],secretAccessKey:[e?e.secretAccessKey:null,[V.required]],region:[e?e.region:null,[V.required]]})}}e("SqsConfigComponent",zn),zn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:zn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),zn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:zn,selector:"tb-external-node-sqs-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:zn,decorators:[{type:n,args:[{selector:"tb-external-node-sqs-config",template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class _n{}e("RulenodeCoreConfigExternalModule",_n),_n.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:_n,deps:[],target:t.ɵɵFactoryTarget.NgModule}),_n.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:_n,declarations:[Un,zn,Pn,Dn,Vn,wn,Rn,On,Hn,En,Kn,Bn],imports:[B,k,Ae,Cn],exports:[Un,zn,Pn,Dn,Vn,wn,Rn,On,Hn,En,Kn,Bn]}),_n.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:_n,imports:[B,k,Ae,Cn]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:_n,decorators:[{type:s,args:[{declarations:[Un,zn,Pn,Dn,Vn,wn,Rn,On,Hn,En,Kn,Bn],imports:[B,k,Ae,Cn],exports:[Un,zn,Pn,Dn,Vn,wn,Rn,On,Hn,En,Kn,Bn]}]}]});class jn extends m{constructor(e,t,n){super(e),this.store=e,this.translate=t,this.fb=n,this.alarmStatusTranslationsMap=A,this.alarmStatusList=[],this.searchText="",this.displayStatusFn=this.displayStatus.bind(this);for(const e of Object.keys(G))this.alarmStatusList.push(G[e]);this.statusFormControl=new H(""),this.filteredAlarmStatus=this.statusFormControl.valueChanges.pipe(ke(""),ve((e=>e||"")),Fe((e=>this.fetchAlarmStatus(e))),Te())}ngOnInit(){super.ngOnInit()}configForm(){return this.alarmStatusConfigForm}prepareInputConfig(e){return this.searchText="",this.statusFormControl.patchValue("",{emitEvent:!0}),e}onConfigurationSet(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e?e.alarmStatusList:null,[V.required]]})}displayStatus(e){return e?this.translate.instant(A.get(e)):void 0}fetchAlarmStatus(e){const t=this.getAlarmStatusList();if(this.searchText=e,this.searchText&&this.searchText.length){const e=this.searchText.toUpperCase();return Se(t.filter((t=>this.translate.instant(A.get(G[t])).toUpperCase().includes(e))))}return Se(t)}alarmStatusSelected(e){this.addAlarmStatus(e.option.value),this.clear("")}removeAlarmStatus(e){const t=this.alarmStatusConfigForm.get("alarmStatusList").value;if(t){const n=t.indexOf(e);n>=0&&(t.splice(n,1),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}}addAlarmStatus(e){let t=this.alarmStatusConfigForm.get("alarmStatusList").value;t||(t=[]);-1===t.indexOf(e)&&(t.push(e),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}getAlarmStatusList(){return this.alarmStatusList.filter((e=>-1===this.alarmStatusConfigForm.get("alarmStatusList").value.indexOf(e)))}onAlarmStatusInputFocus(){this.statusFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})}clear(e=""){this.alarmStatusInput.nativeElement.value=e,this.statusFormControl.patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.alarmStatusInput.nativeElement.blur(),this.alarmStatusInput.nativeElement.focus()}),0)}}e("CheckAlarmStatusComponent",jn),jn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:jn,deps:[{token:E.Store},{token:$.TranslateService},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),jn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:jn,selector:"tb-filter-node-check-alarm-status-config",viewQueries:[{propertyName:"alarmStatusInput",first:!0,predicate:["alarmStatusInput"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.alarm-status-filter\n \n \n \n {{alarmStatusTranslationsMap.get(alarmStatus) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-alarm-status-matching\n
\n
\n
\n
\n
\n \n
\n\n\n\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ge.TbErrorComponent,selector:"tb-error",inputs:["noMargin","error"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:Ie.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple","hideSingleSelectionIndicator"],exportAs:["matAutocomplete"]},{kind:"directive",type:Ie.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{kind:"directive",type:Ie.MatAutocompleteOrigin,selector:"[matAutocompleteOrigin]",exportAs:["matAutocompleteOrigin"]},{kind:"component",type:pe.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:pe.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:pe.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:pe.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormControlDirective,selector:"[formControl]",inputs:["formControl","disabled","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:K.AsyncPipe,name:"async"},{kind:"pipe",type:Ne.HighlightPipe,name:"highlight"},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:jn,decorators:[{type:n,args:[{selector:"tb-filter-node-check-alarm-status-config",template:'
\n \n tb.rulenode.alarm-status-filter\n \n \n \n {{alarmStatusTranslationsMap.get(alarmStatus) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-alarm-status-matching\n
\n
\n
\n
\n
\n \n
\n\n\n\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:$.TranslateService},{type:D.UntypedFormBuilder}]},propDecorators:{alarmStatusInput:[{type:a,args:["alarmStatusInput",{static:!1}]}]}});class $n extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[le,se,me]}configForm(){return this.checkMessageConfigForm}onConfigurationSet(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e?e.messageNames:null,[]],metadataNames:[e?e.metadataNames:null,[]],checkAllKeys:[!!e&&e.checkAllKeys,[]]})}validateConfig(){const e=this.checkMessageConfigForm.get("messageNames").value,t=this.checkMessageConfigForm.get("metadataNames").value;return e.length>0||t.length>0}removeMessageName(e){const t=this.checkMessageConfigForm.get("messageNames").value,n=t.indexOf(e);n>=0&&(t.splice(n,1),this.checkMessageConfigForm.get("messageNames").setValue(t,{emitEvent:!0}))}removeMetadataName(e){const t=this.checkMessageConfigForm.get("metadataNames").value,n=t.indexOf(e);n>=0&&(t.splice(n,1),this.checkMessageConfigForm.get("metadataNames").setValue(t,{emitEvent:!0}))}addMessageName(e){const t=e.input;let n=e.value;if((n||"").trim()){n=n.trim();let e=this.checkMessageConfigForm.get("messageNames").value;e&&-1!==e.indexOf(n)||(e||(e=[]),e.push(n),this.checkMessageConfigForm.get("messageNames").setValue(e,{emitEvent:!0}))}t&&(t.value="")}addMetadataName(e){const t=e.input;let n=e.value;if((n||"").trim()){n=n.trim();let e=this.checkMessageConfigForm.get("metadataNames").value;e&&-1!==e.indexOf(n)||(e||(e=[]),e.push(n),this.checkMessageConfigForm.get("metadataNames").setValue(e,{emitEvent:!0}))}t&&(t.value="")}}e("CheckMessageConfigComponent",$n),$n.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:$n,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),$n.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:$n,selector:"tb-filter-node-check-message-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.data-keys\n \n \n {{messageName}}\n close\n \n \n \n tb.rulenode.separator-hint\n \n \n tb.rulenode.metadata-keys\n \n \n {{metadataName}}\n close\n \n \n \n tb.rulenode.separator-hint\n \n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
tb.rulenode.check-all-keys-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}\n"],dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"component",type:pe.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:pe.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:pe.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:pe.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:$n,decorators:[{type:n,args:[{selector:"tb-filter-node-check-message-config",template:'
\n \n tb.rulenode.data-keys\n \n \n {{messageName}}\n close\n \n \n \n tb.rulenode.separator-hint\n \n \n tb.rulenode.metadata-keys\n \n \n {{metadataName}}\n close\n \n \n \n tb.rulenode.separator-hint\n \n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
tb.rulenode.check-all-keys-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}\n"]}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Qn extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.entitySearchDirection=Object.keys(y),this.entitySearchDirectionTranslationsMap=x}configForm(){return this.checkRelationConfigForm}onConfigurationSet(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[!!e&&e.checkForSingleEntity,[]],direction:[e?e.direction:null,[]],entityType:[e?e.entityType:null,e&&e.checkForSingleEntity?[V.required]:[]],entityId:[e?e.entityId:null,e&&e.checkForSingleEntity?[V.required]:[]],relationType:[e?e.relationType:null,[V.required]]})}validatorTriggers(){return["checkForSingleEntity"]}updateValidators(e){const t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[V.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[V.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})}}e("CheckRelationConfigComponent",Qn),Qn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Qn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Qn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Qn,selector:"tb-filter-node-check-relation-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.check-relation-hint
\n \n relation.direction\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n
\n \n \n \n \n
\n \n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:$e.EntityAutocompleteComponent,selector:"tb-entity-autocomplete",inputs:["entityType","entitySubtype","excludeEntityIds","labelText","requiredText","useFullEntityId","appearance","required","disabled"],outputs:["entityChanged"]},{kind:"component",type:de.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","filterAllowedEntityTypes","showLabel","required","disabled"]},{kind:"component",type:Ve.RelationTypeAutocompleteComponent,selector:"tb-relation-type-autocomplete",inputs:["label","floatLabel","required","disabled","subscriptSizing"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Qn,decorators:[{type:n,args:[{selector:"tb-filter-node-check-relation-config",template:'
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.check-relation-hint
\n \n relation.direction\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n
\n \n \n \n \n
\n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Jn extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.perimeterType=st,this.perimeterTypes=Object.keys(st),this.perimeterTypeTranslationMap=mt,this.rangeUnits=Object.keys(dt),this.rangeUnitTranslationMap=ct}configForm(){return this.geoFilterConfigForm}onConfigurationSet(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[V.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[V.required]],perimeterType:[e?e.perimeterType:null,[V.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterKeyName:[e?e.perimeterKeyName:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]]})}validatorTriggers(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]}updateValidators(e){const t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,n=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterKeyName").setValidators([V.required]):this.geoFilterConfigForm.get("perimeterKeyName").setValidators([]),t||n!==st.CIRCLE?(this.geoFilterConfigForm.get("centerLatitude").setValidators([]),this.geoFilterConfigForm.get("centerLongitude").setValidators([]),this.geoFilterConfigForm.get("range").setValidators([]),this.geoFilterConfigForm.get("rangeUnit").setValidators([])):(this.geoFilterConfigForm.get("centerLatitude").setValidators([V.required,V.min(-90),V.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([V.required,V.min(-180),V.max(180)]),this.geoFilterConfigForm.get("range").setValidators([V.required,V.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([V.required])),t||n!==st.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([V.required]),this.geoFilterConfigForm.get("perimeterKeyName").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})}}e("GpsGeoFilterConfigComponent",Jn),Jn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Jn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Jn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Jn,selector:"tb-filter-node-gps-geofencing-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n \n tb.rulenode.perimeter-key-name\n \n \n {{ \'tb.rulenode.perimeter-key-name-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:U.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:D.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Jn,decorators:[{type:n,args:[{selector:"tb-filter-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n \n tb.rulenode.perimeter-key-name\n \n \n {{ \'tb.rulenode.perimeter-key-name-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Yn extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.messageTypeConfigForm}onConfigurationSet(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e?e.messageTypes:null,[V.required]]})}}e("MessageTypeConfigComponent",Yn),Yn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Yn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Yn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Yn,selector:"tb-filter-node-message-type-config",usesInheritance:!0,ngImport:t,template:'
\n \n
\n',dependencies:[{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:fn,selector:"tb-message-types-config",inputs:["required","label","placeholder","disabled"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Yn,decorators:[{type:n,args:[{selector:"tb-filter-node-message-type-config",template:'
\n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Wn extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.allowedEntityTypes=[b.DEVICE,b.ASSET,b.ENTITY_VIEW,b.TENANT,b.CUSTOMER,b.USER,b.DASHBOARD,b.RULE_CHAIN,b.RULE_NODE]}configForm(){return this.originatorTypeConfigForm}onConfigurationSet(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e?e.originatorTypes:null,[V.required]]})}}e("OriginatorTypeConfigComponent",Wn),Wn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Wn,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Wn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Wn,selector:"tb-filter-node-originator-type-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n',dependencies:[{kind:"component",type:Qe.EntityTypeListComponent,selector:"tb-entity-type-list",inputs:["label","floatLabel","required","disabled","subscriptSizing","allowedEntityTypes","ignoreAuthorityFilter"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Wn,decorators:[{type:n,args:[{selector:"tb-filter-node-originator-type-config",template:'
\n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class Xn extends m{constructor(e,t,n,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=r,this.tbelEnabled=X(this.store).tbelEnabled,this.scriptLanguage=c,this.changeScript=new o,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-filter-function"}configForm(){return this.scriptConfigForm}onConfigurationSet(e){this.scriptConfigForm=this.fb.group({scriptLang:[e?e.scriptLang:c.JS,[V.required]],jsScript:[e?e.jsScript:null,[]],tbelScript:[e?e.tbelScript:null,[]]})}validatorTriggers(){return["scriptLang"]}updateValidators(e){let t=this.scriptConfigForm.get("scriptLang").value;t!==c.TBEL||this.tbelEnabled||(t=c.JS,this.scriptConfigForm.get("scriptLang").patchValue(t,{emitEvent:!1}),setTimeout((()=>{this.scriptConfigForm.updateValueAndValidity({emitEvent:!0})}))),this.scriptConfigForm.get("jsScript").setValidators(t===c.JS?[V.required]:[]),this.scriptConfigForm.get("jsScript").updateValueAndValidity({emitEvent:e}),this.scriptConfigForm.get("tbelScript").setValidators(t===c.TBEL?[V.required]:[]),this.scriptConfigForm.get("tbelScript").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=c.JS)),e}testScript(e){const t=this.scriptConfigForm.get("scriptLang").value,n=t===c.JS?"jsScript":"tbelScript",r=t===c.JS?"rulenode/filter_node_script_fn":"rulenode/tbel/filter_node_script_fn",o=this.scriptConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.scriptConfigForm.get(n).setValue(e),this.changeScript.emit())}))}onValidate(){this.scriptConfigForm.get("scriptLang").value===c.JS&&this.jsFuncComponent.validateOnSubmit()}}e("ScriptConfigComponent",Xn),Xn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Xn,deps:[{token:E.Store},{token:D.UntypedFormBuilder},{token:Z.NodeScriptTestService},{token:$.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Xn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Xn,selector:"tb-filter-node-script-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n
\n \n
\n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:oe.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:ae.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:ie.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Xn,decorators:[{type:n,args:[{selector:"tb-filter-node-script-config",template:'
\n \n \n \n \n \n
\n \n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder},{type:Z.NodeScriptTestService},{type:$.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:a,args:["tbelFuncComponent",{static:!1}]}]}});class Zn extends m{constructor(e,t,n,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=r,this.tbelEnabled=X(this.store).tbelEnabled,this.scriptLanguage=c,this.changeScript=new o,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-switch-function"}configForm(){return this.switchConfigForm}onConfigurationSet(e){this.switchConfigForm=this.fb.group({scriptLang:[e?e.scriptLang:c.JS,[V.required]],jsScript:[e?e.jsScript:null,[]],tbelScript:[e?e.tbelScript:null,[]]})}validatorTriggers(){return["scriptLang"]}updateValidators(e){let t=this.switchConfigForm.get("scriptLang").value;t!==c.TBEL||this.tbelEnabled||(t=c.JS,this.switchConfigForm.get("scriptLang").patchValue(t,{emitEvent:!1}),setTimeout((()=>{this.switchConfigForm.updateValueAndValidity({emitEvent:!0})}))),this.switchConfigForm.get("jsScript").setValidators(t===c.JS?[V.required]:[]),this.switchConfigForm.get("jsScript").updateValueAndValidity({emitEvent:e}),this.switchConfigForm.get("tbelScript").setValidators(t===c.TBEL?[V.required]:[]),this.switchConfigForm.get("tbelScript").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=c.JS)),e}testScript(e){const t=this.switchConfigForm.get("scriptLang").value,n=t===c.JS?"jsScript":"tbelScript",r=t===c.JS?"rulenode/switch_node_script_fn":"rulenode/tbel/switch_node_script_fn",o=this.switchConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.switchConfigForm.get(n).setValue(e),this.changeScript.emit())}))}onValidate(){this.switchConfigForm.get("scriptLang").value===c.JS&&this.jsFuncComponent.validateOnSubmit()}}e("SwitchConfigComponent",Zn),Zn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Zn,deps:[{token:E.Store},{token:D.UntypedFormBuilder},{token:Z.NodeScriptTestService},{token:$.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Zn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Zn,selector:"tb-filter-node-switch-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n
\n \n
\n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:oe.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:ae.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:ie.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Zn,decorators:[{type:n,args:[{selector:"tb-filter-node-switch-config",template:'
\n \n \n \n \n \n
\n \n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder},{type:Z.NodeScriptTestService},{type:$.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:a,args:["tbelFuncComponent",{static:!1}]}]}});class er{}e("RuleNodeCoreConfigFilterModule",er),er.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:er,deps:[],target:t.ɵɵFactoryTarget.NgModule}),er.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:er,declarations:[$n,Qn,Jn,Yn,Wn,Xn,Zn,jn],imports:[B,k,Cn],exports:[$n,Qn,Jn,Yn,Wn,Xn,Zn,jn]}),er.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:er,imports:[B,k,Cn]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:er,decorators:[{type:s,args:[{declarations:[$n,Qn,Jn,Yn,Wn,Xn,Zn,jn],imports:[B,k,Cn],exports:[$n,Qn,Jn,Yn,Wn,Xn,Zn,jn]}]}]});class tr extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.originatorSource=it,this.originatorSources=Object.keys(it),this.originatorSourceTranslationMap=lt,this.allowedEntityTypes=[b.DEVICE,b.ASSET,b.ENTITY_VIEW,b.USER,b.EDGE]}configForm(){return this.changeOriginatorConfigForm}onConfigurationSet(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[V.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationsQuery:[e?e.relationsQuery:null,[]]})}validatorTriggers(){return["originatorSource"]}updateValidators(e){const t=this.changeOriginatorConfigForm.get("originatorSource").value;t===it.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([V.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),t===it.ENTITY?(this.changeOriginatorConfigForm.get("entityType").setValidators([V.required]),this.changeOriginatorConfigForm.get("entityNamePattern").setValidators([V.required,V.pattern(/.*\S.*/)])):(this.changeOriginatorConfigForm.get("entityType").patchValue(null,{emitEvent:e}),this.changeOriginatorConfigForm.get("entityNamePattern").patchValue(null,{emitEvent:e}),this.changeOriginatorConfigForm.get("entityType").setValidators([]),this.changeOriginatorConfigForm.get("entityNamePattern").setValidators([])),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e}),this.changeOriginatorConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.changeOriginatorConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})}}e("ChangeOriginatorConfigComponent",tr),tr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:tr,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),tr.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:tr,selector:"tb-transformation-node-change-originator-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n
\n \n \n \n
\n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:de.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","filterAllowedEntityTypes","showLabel","required","disabled"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:j.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:bn,selector:"tb-relations-query-config-old",inputs:["disabled","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:tr,decorators:[{type:n,args:[{selector:"tb-transformation-node-change-originator-config",template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n
\n \n \n \n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class nr extends m{constructor(e,t,n,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=r,this.tbelEnabled=X(this.store).tbelEnabled,this.scriptLanguage=c,this.changeScript=new o,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-transformer-function"}configForm(){return this.scriptConfigForm}onConfigurationSet(e){this.scriptConfigForm=this.fb.group({scriptLang:[e?e.scriptLang:c.JS,[V.required]],jsScript:[e?e.jsScript:null,[V.required]],tbelScript:[e?e.tbelScript:null,[]]})}validatorTriggers(){return["scriptLang"]}updateValidators(e){let t=this.scriptConfigForm.get("scriptLang").value;t!==c.TBEL||this.tbelEnabled||(t=c.JS,this.scriptConfigForm.get("scriptLang").patchValue(t,{emitEvent:!1}),setTimeout((()=>{this.scriptConfigForm.updateValueAndValidity({emitEvent:!0})}))),this.scriptConfigForm.get("jsScript").setValidators(t===c.JS?[V.required]:[]),this.scriptConfigForm.get("jsScript").updateValueAndValidity({emitEvent:e}),this.scriptConfigForm.get("tbelScript").setValidators(t===c.TBEL?[V.required]:[]),this.scriptConfigForm.get("tbelScript").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=c.JS)),e}testScript(e){const t=this.scriptConfigForm.get("scriptLang").value,n=t===c.JS?"jsScript":"tbelScript",r=t===c.JS?"rulenode/transformation_node_script_fn":"rulenode/tbel/transformation_node_script_fn",o=this.scriptConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.scriptConfigForm.get(n).setValue(e),this.changeScript.emit())}))}onValidate(){this.scriptConfigForm.get("scriptLang").value===c.JS&&this.jsFuncComponent.validateOnSubmit()}}e("TransformScriptConfigComponent",nr),nr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:nr,deps:[{token:E.Store},{token:D.UntypedFormBuilder},{token:Z.NodeScriptTestService},{token:$.TranslateService}],target:t.ɵɵFactoryTarget.Component}),nr.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:nr,selector:"tb-transformation-node-script-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n
\n \n
\n
\n',dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:oe.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:ae.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:ie.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:nr,decorators:[{type:n,args:[{selector:"tb-transformation-node-script-config",template:'
\n \n \n \n \n \n
\n \n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder},{type:Z.NodeScriptTestService},{type:$.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:a,args:["tbelFuncComponent",{static:!1}]}]}});class rr extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.mailBodyTypes=[{name:"tb.mail-body-type.plain-text",value:"false"},{name:"tb.mail-body-type.html",value:"true"},{name:"tb.mail-body-type.dynamic",value:"dynamic"}]}configForm(){return this.toEmailConfigForm}onConfigurationSet(e){this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[V.required]],toTemplate:[e?e.toTemplate:null,[V.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[V.required]],mailBodyType:[e?e.mailBodyType:null],isHtmlTemplate:[e?e.isHtmlTemplate:null],bodyTemplate:[e?e.bodyTemplate:null,[V.required]]}),this.toEmailConfigForm.get("mailBodyType").valueChanges.pipe(ke([e?.subjectTemplate])).subscribe((e=>{"dynamic"===e?(this.toEmailConfigForm.get("isHtmlTemplate").patchValue("",{emitEvent:!1}),this.toEmailConfigForm.get("isHtmlTemplate").setValidators(V.required)):this.toEmailConfigForm.get("isHtmlTemplate").clearValidators(),this.toEmailConfigForm.get("isHtmlTemplate").updateValueAndValidity()}))}}e("ToEmailConfigComponent",rr),rr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:rr,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),rr.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:rr,selector:"tb-transformation-node-to-email-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n \n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n \n \n \n tb.rulenode.mail-body-type\n \n \n {{ type.name | translate }}\n \n \n \n \n tb.rulenode.dynamic-mail-body-type\n \n \n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n \n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:J.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:Y.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:$.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:rr,decorators:[{type:n,args:[{selector:"tb-transformation-node-to-email-config",template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n \n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n \n \n \n tb.rulenode.mail-body-type\n \n \n {{ type.name | translate }}\n \n \n \n \n tb.rulenode.dynamic-mail-body-type\n \n \n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class or extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[le,se,me]}onConfigurationSet(e){this.copyKeysConfigForm=this.fb.group({fromMetadata:[e?e.fromMetadata:null,[V.required]],keys:[e?e.keys:null,[V.required]]})}configForm(){return this.copyKeysConfigForm}removeKey(e){const t=this.copyKeysConfigForm.get("keys").value,n=t.indexOf(e);n>=0&&(t.splice(n,1),this.copyKeysConfigForm.get("keys").patchValue(t,{emitEvent:!0}))}addKey(e){const t=e.input;let n=e.value;if((n||"").trim()){n=n.trim();let e=this.copyKeysConfigForm.get("keys").value;e&&-1!==e.indexOf(n)||(e||(e=[]),e.push(n),this.copyKeysConfigForm.get("keys").patchValue(e,{emitEvent:!0}))}t&&(t.value="")}}e("CopyKeysConfigComponent",or),or.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:or,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),or.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:or,selector:"tb-transformation-node-copy-keys-config",usesInheritance:!0,ngImport:t,template:'
\n
{{\'tb.rulenode.copy-from\' | translate}}
\n \n \n {{\'tb.rulenode.data-to-metadata\' | translate}}\n \n \n {{\'tb.rulenode.metadata-to-data\' | translate}}\n \n \n \n \n \n {{key}}\n close\n \n \n \n {{ \'tb.rulenode.keys-required\' | translate }}\n \n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_e.MatRadioGroup,selector:"mat-radio-group",exportAs:["matRadioGroup"]},{kind:"component",type:_e.MatRadioButton,selector:"mat-radio-button",inputs:["disableRipple","tabIndex"],exportAs:["matRadioButton"]},{kind:"component",type:pe.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:pe.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:pe.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:pe.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:or,decorators:[{type:n,args:[{selector:"tb-transformation-node-copy-keys-config",template:'
\n
{{\'tb.rulenode.copy-from\' | translate}}
\n \n \n {{\'tb.rulenode.data-to-metadata\' | translate}}\n \n \n {{\'tb.rulenode.metadata-to-data\' | translate}}\n \n \n \n \n \n {{key}}\n close\n \n \n \n {{ \'tb.rulenode.keys-required\' | translate }}\n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class ar extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.renameKeysConfigForm}onConfigurationSet(e){this.renameKeysConfigForm=this.fb.group({fromMetadata:[e?e.fromMetadata:null,[V.required]],renameKeysMapping:[e?e.renameKeysMapping:null,[V.required]]})}}e("RenameKeysConfigComponent",ar),ar.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ar,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),ar.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:ar,selector:"tb-transformation-node-rename-keys-config",usesInheritance:!0,ngImport:t,template:'
\n
{{ \'tb.rulenode.rename-keys-in\' | translate }}
\n \n \n {{\'tb.rulenode.data\' | translate}}\n \n \n {{\'tb.rulenode.metadata\' | translate}}\n \n \n \n \n
\n',dependencies:[{kind:"directive",type:_e.MatRadioGroup,selector:"mat-radio-group",exportAs:["matRadioGroup"]},{kind:"component",type:_e.MatRadioButton,selector:"mat-radio-button",inputs:["disableRipple","tabIndex"],exportAs:["matRadioButton"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ar,decorators:[{type:n,args:[{selector:"tb-transformation-node-rename-keys-config",template:'
\n
{{ \'tb.rulenode.rename-keys-in\' | translate }}
\n \n \n {{\'tb.rulenode.data\' | translate}}\n \n \n {{\'tb.rulenode.metadata\' | translate}}\n \n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class ir extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.jsonPathConfigForm}onConfigurationSet(e){this.jsonPathConfigForm=this.fb.group({jsonPath:[e?e.jsonPath:null,[V.required]]})}}e("NodeJsonPathConfigComponent",ir),ir.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ir,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),ir.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:ir,selector:"tb-transformation-node-json-path-config",usesInheritance:!0,ngImport:t,template:"
\n \n {{ 'tb.rulenode.json-path-expression' | translate }}\n \n {{ 'tb.rulenode.json-path-expression-hint' | translate }}\n {{ 'tb.rulenode.json-path-expression-required' | translate }}\n \n
\n\n",dependencies:[{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatLabel,selector:"mat-label"},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ir,decorators:[{type:n,args:[{selector:"tb-transformation-node-json-path-config",template:"
\n \n {{ 'tb.rulenode.json-path-expression' | translate }}\n \n {{ 'tb.rulenode.json-path-expression-hint' | translate }}\n {{ 'tb.rulenode.json-path-expression-required' | translate }}\n \n
\n\n"}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class lr extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[le,se,me]}onConfigurationSet(e){this.deleteKeysConfigForm=this.fb.group({fromMetadata:[e?e.fromMetadata:null,[V.required]],keys:[e?e.keys:null,[V.required]]})}configForm(){return this.deleteKeysConfigForm}removeKey(e){const t=this.deleteKeysConfigForm.get("keys").value,n=t.indexOf(e);n>=0&&(t.splice(n,1),this.deleteKeysConfigForm.get("keys").patchValue(t,{emitEvent:!0}))}addKey(e){const t=e.input;let n=e.value;if((n||"").trim()){n=n.trim();let e=this.deleteKeysConfigForm.get("keys").value;e&&-1!==e.indexOf(n)||(e||(e=[]),e.push(n),this.deleteKeysConfigForm.get("keys").patchValue(e,{emitEvent:!0}))}t&&(t.value="")}}e("DeleteKeysConfigComponent",lr),lr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:lr,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),lr.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:lr,selector:"tb-transformation-node-delete-keys-config",usesInheritance:!0,ngImport:t,template:'
\n
{{\'tb.rulenode.delete-from\' | translate}}
\n \n \n {{\'tb.rulenode.data\' | translate}}\n \n \n {{\'tb.rulenode.metadata\' | translate}}\n \n \n \n \n \n {{key}}\n close\n \n \n \n {{ \'tb.rulenode.keys-required\' | translate }}\n \n \n
\n',dependencies:[{kind:"directive",type:K.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:K.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ue.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:z.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:_.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:_.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:_.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_e.MatRadioGroup,selector:"mat-radio-group",exportAs:["matRadioGroup"]},{kind:"component",type:_e.MatRadioButton,selector:"mat-radio-button",inputs:["disableRipple","tabIndex"],exportAs:["matRadioButton"]},{kind:"component",type:pe.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:pe.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:pe.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:pe.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:j.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"},{kind:"pipe",type:Ye,name:"safeHtml"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:lr,decorators:[{type:n,args:[{selector:"tb-transformation-node-delete-keys-config",template:'
\n
{{\'tb.rulenode.delete-from\' | translate}}
\n \n \n {{\'tb.rulenode.data\' | translate}}\n \n \n {{\'tb.rulenode.metadata\' | translate}}\n \n \n \n \n \n {{key}}\n close\n \n \n \n {{ \'tb.rulenode.keys-required\' | translate }}\n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class sr{}e("RulenodeCoreConfigTransformModule",sr),sr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:sr,deps:[],target:t.ɵɵFactoryTarget.NgModule}),sr.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:sr,declarations:[tr,nr,rr,or,ar,ir,lr],imports:[B,k,Cn],exports:[tr,nr,rr,or,ar,ir,lr]}),sr.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:sr,imports:[B,k,Cn]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:sr,decorators:[{type:s,args:[{declarations:[tr,nr,rr,or,ar,ir,lr],imports:[B,k,Cn],exports:[tr,nr,rr,or,ar,ir,lr]}]}]});class mr extends m{constructor(e,t){super(e),this.store=e,this.fb=t,this.entityType=b}configForm(){return this.ruleChainInputConfigForm}onConfigurationSet(e){this.ruleChainInputConfigForm=this.fb.group({ruleChainId:[e?e.ruleChainId:null,[V.required]]})}}e("RuleChainInputComponent",mr),mr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:mr,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),mr.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:mr,selector:"tb-flow-node-rule-chain-input-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n',dependencies:[{kind:"component",type:$e.EntityAutocompleteComponent,selector:"tb-entity-autocomplete",inputs:["entityType","entitySubtype","excludeEntityIds","labelText","requiredText","useFullEntityId","appearance","required","disabled"],outputs:["entityChanged"]},{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:D.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:mr,decorators:[{type:n,args:[{selector:"tb-flow-node-rule-chain-input-config",template:'
\n \n \n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class ur extends m{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.ruleChainOutputConfigForm}onConfigurationSet(e){this.ruleChainOutputConfigForm=this.fb.group({})}}e("RuleChainOutputComponent",ur),ur.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ur,deps:[{token:E.Store},{token:D.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),ur.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:ur,selector:"tb-flow-node-rule-chain-output-config",usesInheritance:!0,ngImport:t,template:'
\n
\n
\n',dependencies:[{kind:"directive",type:j.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:D.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:D.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"pipe",type:$.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ur,decorators:[{type:n,args:[{selector:"tb-flow-node-rule-chain-output-config",template:'
\n
\n
\n'}]}],ctorParameters:function(){return[{type:E.Store},{type:D.UntypedFormBuilder}]}});class pr{}e("RuleNodeCoreConfigFlowModule",pr),pr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:pr,deps:[],target:t.ɵɵFactoryTarget.NgModule}),pr.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:pr,declarations:[mr,ur],imports:[B,k,Cn],exports:[mr,ur]}),pr.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:pr,imports:[B,k,Cn]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:pr,decorators:[{type:s,args:[{declarations:[mr,ur],imports:[B,k,Cn],exports:[mr,ur]}]}]});class dr{constructor(e){!function(e){e.setTranslation("en_US",{tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","output-message-type":"Output message type","output-message-type-required":"Output message type is required","output-message-type-max-length":"Output message type should be less than 256","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","interval-start":"Interval start","interval-end":"Interval end","time-unit":"Time unit","fetch-mode":"Fetch mode","order-by-timestamp":"Order by timestamp",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. If you want to fetch a single entry, select fetch mode 'First' or 'Last'.","limit-required":"Limit is required!","limit-range":"Limit should be in a range from 2 to 1000!","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647!","start-interval-value-required":"Start interval value is required!","end-interval-value-required":"End interval value is required!",filter:"Filter",switch:"Switch","math-templatization-tooltip":"This field support templatization. Use $[messageKey] to extract value from the message body and ${metadataKey} to extract value from the message metadata.","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","attributes-keys":"Attributes keys","attributes-keys-required":"Attributes keys are required","notify-device":"Notify device","send-attributes-updated-notification":"Send attributes updated notification","send-attributes-updated-notification-hint":"Send notification about updated attributes as a separate message to the rule engine queue.","send-attributes-deleted-notification":"Send attributes deleted notification","send-attributes-deleted-notification-hint":"Send notification about deleted attributes as a separate message to the rule engine queue.","fetch-credentials-to-metadata":"Fetch credentials to metadata","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","notify-device-delete-hint":"Send notification about deleted attributes to device.","latest-timeseries":"Latest time-series data keys","timeseries-keys":"Timeseries keys","add-timeseries-key":"Add timeseries key","data-keys":"Message field names","copy-from":"Copy from","data-to-metadata":"Data to metadata","metadata-to-data":"Metadata to data","use-regular-expression-hint":"Hint: use regular expression to copy keys by pattern",interval:"Interval","interval-required":"Interval is required","interval-hint":"Deduplication interval in seconds.","interval-min-error":"Min allowed value is 1","max-pending-msgs":"Max pending messages","max-pending-msgs-hint":"Maximum number of messages that are stored in memory for each unique deduplication id.","max-pending-msgs-required":"Max pending messages is required","max-pending-msgs-max-error":"Max allowed value is 1000","max-pending-msgs-min-error":"Min allowed value is 1","max-retries":"Max retries","max-retries-required":"Max retries is required","max-retries-hint":"Maximum number of retries to push the deduplicated messages into the queue. 10 seconds delay is used between retries","max-retries-max-error":"Max allowed value is 100","max-retries-min-error":"Min allowed value is 0",strategy:"Strategy","strategy-required":"Strategy is required","strategy-all-hint":"Return all messages that arrived during deduplication period as a single JSON array message. Where each element represents object with msg and metadata inner properties.","strategy-first-hint":"Return first message that arrived during deduplication period.","strategy-last-hint":"Return last message that arrived during deduplication period.",first:"First",last:"Last",all:"All","output-msg-type-hint":"The message type of the deduplication result.","queue-name-hint":"The queue name where the deduplication result will be published.",keys:"Keys","keys-required":"Keys are required","rename-keys-in":"Rename keys in",data:"Data",message:"Message",metadata:"Metadata","key-name":"Key name","key-name-required":"Key name is required","new-key-name":"New key name","new-key-name-required":"New key name is required","metadata-keys":"Metadata field names","json-path-expression":"JSON path expression","json-path-expression-required":"JSON path expression is required","json-path-expression-hint":"JSONPath specifies a path to an element or a set of elements in a JSON structure. '$' represents the root object or array.","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","max-relation-level-error":"Max relation level should be greater than 0 or unspecified!","relation-type":"Relation type","relation-type-pattern":"Relation type pattern","relation-type-pattern-required":"Relation type pattern is required","relation-types-list":"Relation types to propagate","relation-types-list-hint":"If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","add-telemetry-key":"Add telemetry key","delete-from":"Delete from","use-regular-expression-delete-hint":"Use regular expression to delete keys by pattern","fetch-into":"Fetch into","attr-mapping":"Attributes mapping:","source-attribute":"Source attribute key","source-attribute-required":"Source attribute key is required!","source-telemetry":"Source telemetry key","source-telemetry-required":"Source telemetry key is required!","target-key":"Target key","target-key-required":"Target key is required!","attr-mapping-required":"At least one mapping entry should be specified!","fields-mapping":"Fields mapping*","fields-mapping-required":"At least one field mapping should be specified.","originator-fields-sv-map-hint":"Target key fields support templatization. Use $[messageKey] to extract value from the message body and ${metadataKey} to extract value from the message metadata.","sv-map-hint":"Only target key fields support templatization. Use $[messageKey] to extract value from the message body and ${metadataKey} to extract value from the message metadata.","source-field":"Source field","source-field-required":"Source field is required!","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","originator-entity":"Entity","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata or data assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required","alarm-severity-pattern":"Alarm severity pattern","alarm-status-filter":"Alarm status filter","alarm-status-list-empty":"Alarm status list is empty","no-alarm-status-matching":"No alarm status matching were found.",propagate:"Propagate alarm to related entities","propagate-to-owner":"Propagate alarm to entity owner (Customer or Tenant)","propagate-to-tenant":"Propagate alarm to Tenant",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":'Comma separated address list, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","body-template":"Body Template","body-template-required":"Body Template is required","dynamic-mail-body-type":"Dynamic mail body type","mail-body-type":"Mail body type","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","ignore-request-body":"Without request body","trim-double-quotes":"Message without quotes","trim-double-quotes-hint":"If selected, request body message payload will be sent without double quotes, i.e. msg = message body","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in header/value fields',header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","key-pattern":"Key pattern","key-pattern-hint":"Hint: Optional. If a valid partition number is specified, it will be used when sending the record. If no partition is specified, the key will be used instead. If neither is specified, a partition will be assigned in a round-robin fashion.","topic-pattern-required":"Topic pattern is required",topic:"Topic","topic-required":"Topic is required","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in name/value fields',"connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","client-id-hint":'Hint: Optional. Leave empty for auto-generated Client ID. Be careful when specifying the Client ID. Majority of the MQTT brokers will not allow multiple connections with the same Client ID. To connect to such brokers, your mqtt Client ID must be unique. When platform is running in a micro-services mode, the copy of rule node is launched in each micro-service. This will automatically lead to multiple mqtt clients with the same ID and may cause failures of the rule node. To avoid such failures enable "Add Service ID as suffix to Client ID" option below.',"append-client-id-suffix":"Add Service ID as suffix to Client ID","client-id-suffix-hint":'Hint: Optional. Applied when "Client ID" specified explicitly. If selected then Service ID will be added to Client ID as a suffix. Helps to avoid failures when platform is running in a micro-services mode.',"device-id":"Device ID","device-id-required":"Device ID is required.","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","credentials-pem-hint":"At least Server CA certificate file or a pair of Client certificate and Client private key files are required","credentials-sas":"Shared Access Signature","sas-key":"SAS Key","sas-key-required":"SAS Key is required.",hostname:"Hostname","hostname-required":"Hostname is required.","azure-ca-cert":"CA certificate file","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"Server CA certificate file","private-key":"Client private key file",cert:"Client certificate file","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-dynamic-interval":"Use dynamic interval","metadata-dynamic-interval-hint":"Interval start and end input fields support templatization. Note that the substituted template value should be set in milliseconds. Use $[messageKey] to extract value from the message body and ${metadataKey} to extract value from the message metadata.","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata or data assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","overwrite-alarm-details":"Overwrite alarm details","use-alarm-severity-pattern":"Use alarm severity pattern","check-all-keys":"Check that all specified fields are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval":"Interval start","end-interval":"Interval end","start-interval-required":"Interval start is required!","end-interval-required":"Interval end is required!","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","tls-version":"TLS version","enable-proxy":"Enable proxy","use-system-proxy-properties":"Use system proxy properties","proxy-host":"Proxy host","proxy-host-required":"Proxy host is required.","proxy-port":"Proxy port","proxy-port-required":"Proxy port is required.","proxy-port-range":"Proxy port should be in a range from 1 to 65535.","proxy-user":"Proxy user","proxy-password":"Proxy password","proxy-scheme":"Proxy scheme","numbers-to-template":"Phone Numbers To Template","numbers-to-template-required":"Phone Numbers To Template is required","numbers-to-template-hint":'Comma separated Phone Numbers, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"sms-message-template":"SMS message Template","sms-message-template-required":"SMS message Template is required","use-system-sms-settings":"Use system SMS provider settings","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'Press "Enter" to complete field input.',"entity-details":"Select entity details","entity-details-id":"Id","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-city":"City","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","entity-details-list-empty":"No entity details selected!","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"Enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-key-name":"Perimeter key name","perimeter-key-name-required":"Perimeter key name is required.","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch timestamp for the latest telemetry values","get-latest-value-with-ts-hint":'If selected, the latest telemetry values will also include timestamp, e.g: "temp": "{"ts":1574329385897, "value":42}"',"use-redis-queue":"Use redis queue for message persistence","ignore-null-strings":"Ignore null strings","ignore-null-strings-hint":"If selected rule node will ignore entity fields with empty value.","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size","add-metadata-key-values-as-kafka-headers":"Add Message metadata key-value pairs to Kafka record headers","add-metadata-key-values-as-kafka-headers-hint":"If selected, key-value pairs from message metadata will be added to the outgoing records headers as byte arrays with predefined charset encoding.","charset-encoding":"Charset encoding","charset-encoding-required":"Charset encoding is required.","charset-us-ascii":"US-ASCII","charset-iso-8859-1":"ISO-8859-1","charset-utf-8":"UTF-8","charset-utf-16be":"UTF-16BE","charset-utf-16le":"UTF-16LE","charset-utf-16":"UTF-16","select-queue-hint":"The queue name can be selected from a drop-down list or add a custom name.","persist-alarm-rules":"Persist state of alarm rules","fetch-alarm-rules":"Fetch state of alarm rules","input-value-key":"Input value key","input-value-key-required":"Input value key is required!","output-value-key":"Output value key","output-value-key-required":"Output value key is required!","number-of-digits-after-floating-point":"Number of digits after floating point","number-of-digits-after-floating-point-range":"Number of digits after floating point should be in a range from 0 to 15!","failure-if-delta-negative":"Tell Failure if delta is negative","failure-if-delta-negative-tooltip":"Rule node forces failure of message processing if delta value is negative.","use-cashing":"Use cashing","use-cashing-tooltip":'Rule node will cache the value of "{{inputValueKey}}" that arrives from the incoming message to improve performance. Note that the cache will not be updated if you modify the "{{inputValueKey}}" value elsewhere.',"add-time-difference-between-readings":'Add the time difference between "{{inputValueKey}}" readings',"add-time-difference-between-readings-tooltip":'If enabled, the rule node will add the "{{periodValueKey}}" to the outbound message.',"period-value-key":"Period value key","period-value-key-required":"Period value key is required!","general-pattern-hint":"Use ${metadataKey} for value from metadata, $[messageKey] for value from message body.","alarm-severity-pattern-hint":'Hint: use ${metadataKey} for value from metadata, $[messageKey] for value from message body. Alarm severity should be system (CRITICAL, MAJOR etc.)',"output-node-name-hint":"The rule node name corresponds to the relation type of the output message, and it is used to forward messages to other rule nodes in the caller rule chain.","skip-latest-persistence":"Skip latest persistence","use-server-ts":"Use server ts","use-server-ts-hint":"Enable this setting to use the timestamp of the message processing instead of the timestamp from the message. Useful for all sorts of sequential processing if you merge messages from multiple sources (devices, assets, etc).","kv-map-pattern-hint":"All input fields support templatization. Use $[messageKey] to extract value from the message body and ${metadataKey} to extract value from the message metadata.","shared-scope":"Shared scope","server-scope":"Server scope","client-scope":"Client scope","attribute-type":"Attribute","constant-type":"Constant","time-series-type":"Time series","message-body-type":"Message body","message-metadata-type":"Message metadata","argument-tile":"Arguments","no-arguments-prompt":"No arguments configured","result-title":"Result","functions-field-input":"Functions","no-option-found":"No option found","argument-type-field-input":"Type","argument-type-field-input-required":"Argument type is required.","argument-key-field-input":"Key","argument-key-field-input-required":"Argument key is required.","constant-value-field-input":"Constant value","constant-value-field-input-required":"Constant value is required.","attribute-scope-field-input":"Attribute scope","attribute-scope-field-input-required":"Attribute scope os required.","default-value-field-input":"Default value","type-field-input":"Type","type-field-input-required":"Type is required.","key-field-input":"Key","key-field-input-required":"Key is required.","number-floating-point-field-input":"Number of digits after floating point","number-floating-point-field-input-hint":"Hint: use 0 to convert result to integer","add-to-body-field-input":"Add to message body","add-to-metadata-field-input":"Add to message metadata","custom-expression-field-input":"Mathematical Expression","custom-expression-field-input-required":"Mathematical expression is required","custom-expression-field-input-hint":"Hint: specify a mathematical expression to evaluate. For example, transform Fahrenheit to Celsius using (x - 32) / 1.8)","retained-message":"Retained","attributes-mapping":"Attributes mapping*","latest-telemetry-mapping":"Latest telemetry mapping*","add-mapped-attribute-to":"Add mapped attributes to:","add-mapped-latest-telemetry-to":"Add mapped latest telemetry to:","add-mapped-fields-to":"Add mapped fields to:","add-selected-details-to":"Add selected details to:","clear-selected-details":"Clear selected details","clear-selected-keys":"Clear selected keys","fetch-credentials-to":"Fetch credentials to:","add-originator-attributes-to":"Add originator attributes to:","originator-attributes":"Originator attributes","fetch-latest-telemetry-with-timestamp":"Fetch latest telemetry with timestamp","fetch-latest-telemetry-with-timestamp-tooltip":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "{{latestTsKeyName}}": "{"ts":1574329385897, "value":42}"',"tell-failure":"Tell Failure","tell-failure-tooltip":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"created-time":"Created time",type:"Type","first-name":"First name","last-name":"Last name",label:"Label","originator-fields-mapping":"Originator fields mapping","add-mapped-originator-fields-to":"Add mapped originator fields to:",fields:"Fields","skip-empty-fields":"Skip empty fields","skip-empty-fields-tooltip":"Fields with empty values will not be added to the output message/output message metadata.","fetch-interval":"Fetch interval","fetch-strategy":"Fetch strategy","fetch-timeseries-from-to":"Fetch timeseries from {{startInterval}} {{startIntervalTimeUnit}} ago to {{endInterval}} {{endIntervalTimeUnit}} ago.","fetch-timeseries-from-to-invalid":'Fetch timeseries invalid ("Interval start" should be less than "Interval end")!',"use-metadata-dynamic-interval-tooltip":"If selected, the rule node will use dynamic interval start and end based on the message and patterns.","all-mode-hint":'If selected fetch mode "All" rule node will retrieve telemetry from the fetch interval with configurable query parameters.',"first-mode-hint":'If selected fetch mode "First" rule node will retrieve the closest telemetry to the fetch interval\'s start.',"last-mode-hint":'If selected fetch mode "Last" rule node will retrieve the closest telemetry to the fetch interval\'s end.',ascending:"Ascending",descending:"Descending",min:"Min",max:"Max",average:"Average",sum:"Sum",count:"Count",none:"None","last-level-relation-tooltip":"If selected, the rule node will search related entities only on the level set in the max relation level.","last-level-device-relation-tooltip":"If selected, the rule node will search related devices only on the level set in the max relation level.","data-to-fetch":"Data to fetch:","mapping-of-customers":"Mapping of customer's:",attributes:"Attributes","related-device-attributes":"Related device attributes","add-selected-attributes-to":"Add selected attributes to:","device-profiles":"Device profiles","mapping-of-tenant":"Mapping of tenant's:","add-attribute-key":"Add attribute key","message-template":"Message template","message-template-required":"Message template is required","use-system-slack-settings":"Use system slack settings","slack-api-token":"Slack API token","slack-api-token-required":"Slack API token is required"},"key-val":{key:"Key",value:"Value","see-examples":"See examples.","remove-entry":"Remove entry","remove-mapping-entry":"Remove mapping entry","add-mapping-entry":"Add mapping entry","add-entry":"Add entry","unique-key-value-pair-error":"'{{valText}}' must be different from the current '{{keyText}}'"},"mail-body-type":{"plain-text":"Plain Text",html:"HTML",dynamic:"Dynamic"}}},!0)}(e)}}e("RuleNodeCoreConfigModule",dr),dr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:dr,deps:[{token:$.TranslateService}],target:t.ɵɵFactoryTarget.NgModule}),dr.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:dr,declarations:[Je],imports:[B,k],exports:[vn,er,Gn,_n,sr,pr,Je]}),dr.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:dr,imports:[B,k,vn,er,Gn,_n,sr,pr]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:dr,decorators:[{type:s,args:[{declarations:[Je],imports:[B,k],exports:[vn,er,Gn,_n,sr,pr,Je]}]}],ctorParameters:function(){return[{type:$.TranslateService}]}})}}}));//# sourceMappingURL=rulenode-core-config.js.map +System.register(["@angular/core","@shared/public-api","@ngrx/store","@angular/forms","@angular/common","@angular/material/checkbox","@angular/material/input","@angular/material/form-field","@angular/flex-layout/flex","@ngx-translate/core","@angular/material/select","@angular/material/core","@shared/components/queue/queue-autocomplete.component","@core/public-api","@shared/components/js-func.component","@angular/material/button","@angular/material/icon","@angular/material/tooltip","@shared/components/script-lang.component","@angular/cdk/keycodes","@angular/material/chips","@shared/pipe/safe.pipe","@shared/components/entity/entity-type-select.component","@shared/components/entity/entity-select.component","@angular/cdk/coercion","@shared/components/tb-error.component","@angular/flex-layout/extended","@angular/material/list","@angular/cdk/drag-drop","rxjs/operators","@angular/material/autocomplete","@shared/pipe/highlight.pipe","@angular/material/expansion","tslib","rxjs","@home/components/public-api","@shared/components/help-popup.component","@shared/components/entity/entity-subtype-list.component","@shared/components/relation/relation-type-autocomplete.component","@angular/material/slide-toggle","@shared/components/hint-tooltip-icon.component","@home/components/relation/relation-filters.component","@shared/components/file-input.component","@shared/components/button/toggle-password.component","@shared/components/string-items-list.component","@shared/components/toggle-header.component","@shared/components/toggle-select.component","@shared/components/entity/entity-list.component","@shared/components/notification/template-autocomplete.component","@shared/components/tb-checkbox.component","@home/components/sms/sms-provider-configuration.component","@angular/material/radio","@shared/components/slack-conversation-autocomplete.component","@shared/components/entity/entity-autocomplete.component","@shared/components/entity/entity-type-list.component"],(function(e){"use strict";var t,n,r,o,a,i,l,s,m,u,p,d,c,f,g,y,x,b,h,v,C,F,k,L,T,I,N,S,q,A,M,G,E,D,V,P,w,R,O,B,K,U,_,H,z,j,Q,$,J,Y,W,X,Z,ee,te,ne,re,oe,ae,ie,le,se,me,ue,pe,de,ce,fe,ge,ye,xe,be,he,ve,Ce,Fe,ke,Le,Te,Ie,Ne,Se,qe,Ae,Me,Ge,Ee,De,Ve,Pe,we,Re,Oe,Be,Ke,Ue,_e,He,ze,je,Qe,$e;return{setters:[function(e){t=e,n=e.Component,r=e.EventEmitter,o=e.ViewChild,a=e.forwardRef,i=e.Input,l=e.NgModule},function(e){s=e.RuleNodeConfigurationComponent,m=e.AttributeScope,u=e.telemetryTypeTranslations,p=e.ServiceType,d=e.ScriptLanguage,c=e.AlarmSeverity,f=e.alarmSeverityTranslations,g=e.EntitySearchDirection,y=e.entitySearchDirectionTranslations,x=e.EntityType,b=e.PageComponent,h=e.coerceBoolean,v=e.MessageType,C=e.messageTypeNames,F=e,k=e.AlarmStatus,L=e.alarmStatusTranslations,T=e.SharedModule,I=e.AggregationType,N=e.aggregationTranslations,S=e.entityFields,q=e.NotificationType,A=e.SlackChanelType,M=e.SlackChanelTypesTranslateMap},function(e){G=e},function(e){E=e,D=e.Validators,V=e.NgControl,P=e.NG_VALUE_ACCESSOR,w=e.NG_VALIDATORS},function(e){R=e,O=e.CommonModule},function(e){B=e},function(e){K=e},function(e){U=e},function(e){_=e},function(e){H=e},function(e){z=e},function(e){j=e},function(e){Q=e},function(e){$=e.getCurrentAuthState,J=e,Y=e.isDefinedAndNotNull,W=e.deepTrim,X=e.isObject,Z=e.isNotEmptyStr},function(e){ee=e},function(e){te=e},function(e){ne=e},function(e){re=e},function(e){oe=e},function(e){ae=e.ENTER,ie=e.COMMA,le=e.SEMICOLON},function(e){se=e},function(e){me=e},function(e){ue=e},function(e){pe=e},function(e){de=e.coerceBooleanProperty},function(e){ce=e},function(e){fe=e},function(e){ge=e},function(e){ye=e},function(e){xe=e.tap,be=e.map,he=e.mergeMap,ve=e.startWith,Ce=e.share,Fe=e.takeUntil},function(e){ke=e},function(e){Le=e},function(e){Te=e},function(e){Ie=e.__decorate},function(e){Ne=e.of,Se=e.Subject},function(e){qe=e.HomeComponentsModule},function(e){Ae=e},function(e){Me=e},function(e){Ge=e},function(e){Ee=e},function(e){De=e},function(e){Ve=e},function(e){Pe=e},function(e){we=e},function(e){Re=e},function(e){Oe=e},function(e){Be=e},function(e){Ke=e},function(e){Ue=e},function(e){_e=e},function(e){He=e},function(e){ze=e},function(e){je=e},function(e){Qe=e},function(e){$e=e}],execute:function(){class Je extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.emptyConfigForm}onConfigurationSet(e){this.emptyConfigForm=this.fb.group({})}}e("EmptyConfigComponent",Je),Je.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Je,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Je.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Je,selector:"tb-node-empty-config",usesInheritance:!0,ngImport:t,template:"
",isInline:!0}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Je,decorators:[{type:n,args:[{selector:"tb-node-empty-config",template:"
"}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Ye extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.assignCustomerConfigForm}onConfigurationSet(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[D.required,D.pattern(/.*\S.*/)]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[D.required,D.min(0)]]})}prepareOutputConfig(e){return e.customerNamePattern=e.customerNamePattern.trim(),e}}e("AssignCustomerConfigComponent",Ye),Ye.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ye,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ye.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Ye,selector:"tb-action-node-assign-to-customer-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ye,decorators:[{type:n,args:[{selector:"tb-action-node-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class We extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopeMap=m,this.attributeScopes=Object.keys(m),this.telemetryTypeTranslationsMap=u}configForm(){return this.attributesConfigForm}onConfigurationSet(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[D.required]],notifyDevice:[!e||e.notifyDevice,[]],sendAttributesUpdatedNotification:[!!e&&e.sendAttributesUpdatedNotification,[]]}),this.attributesConfigForm.get("scope").valueChanges.subscribe((e=>{e!==m.SHARED_SCOPE&&this.attributesConfigForm.get("notifyDevice").patchValue(!1,{emitEvent:!1}),e===m.CLIENT_SCOPE&&this.attributesConfigForm.get("sendAttributesUpdatedNotification").patchValue(!1,{emitEvent:!1})}))}}e("AttributesConfigComponent",We),We.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:We,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),We.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:We,selector:"tb-action-node-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-hint
\n
\n
\n \n {{ \'tb.rulenode.send-attributes-updated-notification\' | translate }}\n \n
tb.rulenode.send-attributes-updated-notification-hint
\n
\n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:We,decorators:[{type:n,args:[{selector:"tb-action-node-attributes-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-hint
\n
\n
\n \n {{ \'tb.rulenode.send-attributes-updated-notification\' | translate }}\n \n
tb.rulenode.send-attributes-updated-notification-hint
\n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Xe extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.serviceType=p.TB_RULE_ENGINE}configForm(){return this.checkPointConfigForm}onConfigurationSet(e){this.checkPointConfigForm=this.fb.group({queueName:[e?e.queueName:null,[D.required]]})}}e("CheckPointConfigComponent",Xe),Xe.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Xe,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Xe.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Xe,selector:"tb-action-node-check-point-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n',dependencies:[{kind:"component",type:Q.QueueAutocompleteComponent,selector:"tb-queue-autocomplete",inputs:["labelText","requiredText","autocompleteHint","subscriptSizing","required","queueType","disabled"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Xe,decorators:[{type:n,args:[{selector:"tb-action-node-check-point-config",template:'
\n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Ze extends s{constructor(e,t,n,o){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=o,this.tbelEnabled=$(this.store).tbelEnabled,this.scriptLanguage=d,this.changeScript=new r,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-details-function"}configForm(){return this.clearAlarmConfigForm}onConfigurationSet(e){this.clearAlarmConfigForm=this.fb.group({scriptLang:[e?e.scriptLang:d.JS,[D.required]],alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[]],alarmDetailsBuildTbel:[e?e.alarmDetailsBuildTbel:null,[]],alarmType:[e?e.alarmType:null,[D.required]]})}validatorTriggers(){return["scriptLang"]}updateValidators(e){let t=this.clearAlarmConfigForm.get("scriptLang").value;t!==d.TBEL||this.tbelEnabled||(t=d.JS,this.clearAlarmConfigForm.get("scriptLang").patchValue(t,{emitEvent:!1}),setTimeout((()=>{this.clearAlarmConfigForm.updateValueAndValidity({emitEvent:!0})}))),this.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValidators(t===d.JS?[D.required]:[]),this.clearAlarmConfigForm.get("alarmDetailsBuildJs").updateValueAndValidity({emitEvent:e}),this.clearAlarmConfigForm.get("alarmDetailsBuildTbel").setValidators(t===d.TBEL?[D.required]:[]),this.clearAlarmConfigForm.get("alarmDetailsBuildTbel").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=d.JS)),e}testScript(e){const t=this.clearAlarmConfigForm.get("scriptLang").value,n=t===d.JS?"alarmDetailsBuildJs":"alarmDetailsBuildTbel",r=t===d.JS?"rulenode/clear_alarm_node_script_fn":"rulenode/tbel/clear_alarm_node_script_fn",o=this.clearAlarmConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.clearAlarmConfigForm.get(n).setValue(e),this.changeScript.emit())}))}onValidate(){this.clearAlarmConfigForm.get("scriptLang").value===d.JS&&this.jsFuncComponent.validateOnSubmit()}}e("ClearAlarmConfigComponent",Ze),Ze.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ze,deps:[{token:G.Store},{token:E.UntypedFormBuilder},{token:J.NodeScriptTestService},{token:H.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Ze.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Ze,selector:"tb-action-node-clear-alarm-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ee.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:te.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:oe.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ze,decorators:[{type:n,args:[{selector:"tb-action-node-clear-alarm-config",template:'
\n \n \n \n \n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder},{type:J.NodeScriptTestService},{type:H.TranslateService}]},propDecorators:{jsFuncComponent:[{type:o,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:o,args:["tbelFuncComponent",{static:!1}]}]}});class et extends s{constructor(e,t,n,o){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=o,this.alarmSeverities=Object.keys(c),this.alarmSeverityTranslationMap=f,this.separatorKeysCodes=[ae,ie,le],this.tbelEnabled=$(this.store).tbelEnabled,this.scriptLanguage=d,this.changeScript=new r,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-details-function"}configForm(){return this.createAlarmConfigForm}onConfigurationSet(e){this.createAlarmConfigForm=this.fb.group({scriptLang:[e?e.scriptLang:d.JS,[D.required]],alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[]],alarmDetailsBuildTbel:[e?e.alarmDetailsBuildTbel:null,[]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],overwriteAlarmDetails:[!!e&&e.overwriteAlarmDetails,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]],propagateToOwner:[!!e&&e.propagateToOwner,[]],propagateToTenant:[!!e&&e.propagateToTenant,[]],dynamicSeverity:!1}),this.createAlarmConfigForm.get("dynamicSeverity").valueChanges.subscribe((e=>{e?this.createAlarmConfigForm.get("severity").patchValue("",{emitEvent:!1}):this.createAlarmConfigForm.get("severity").patchValue(this.alarmSeverities[0],{emitEvent:!1})}))}validatorTriggers(){return["useMessageAlarmData","overwriteAlarmDetails","scriptLang"]}updateValidators(e){const t=this.createAlarmConfigForm.get("useMessageAlarmData").value,n=this.createAlarmConfigForm.get("overwriteAlarmDetails").value;t?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([D.required]),this.createAlarmConfigForm.get("severity").setValidators([D.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e});let r=this.createAlarmConfigForm.get("scriptLang").value;r!==d.TBEL||this.tbelEnabled||(r=d.JS,this.createAlarmConfigForm.get("scriptLang").patchValue(r,{emitEvent:!1}),setTimeout((()=>{this.createAlarmConfigForm.updateValueAndValidity({emitEvent:!0})})));const o=!1===t||!0===n;this.createAlarmConfigForm.get("alarmDetailsBuildJs").setValidators(o&&r===d.JS?[D.required]:[]),this.createAlarmConfigForm.get("alarmDetailsBuildTbel").setValidators(o&&r===d.TBEL?[D.required]:[]),this.createAlarmConfigForm.get("alarmDetailsBuildJs").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("alarmDetailsBuildTbel").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=d.JS)),e}testScript(e){const t=this.createAlarmConfigForm.get("scriptLang").value,n=t===d.JS?"alarmDetailsBuildJs":"alarmDetailsBuildTbel",r=t===d.JS?"rulenode/create_alarm_node_script_fn":"rulenode/tbel/create_alarm_node_script_fn",o=this.createAlarmConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.createAlarmConfigForm.get(n).setValue(e),this.changeScript.emit())}))}removeKey(e,t){const n=this.createAlarmConfigForm.get(t).value,r=n.indexOf(e);r>=0&&(n.splice(r,1),this.createAlarmConfigForm.get(t).setValue(n,{emitEvent:!0}))}addKey(e,t){const n=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.createAlarmConfigForm.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.createAlarmConfigForm.get(t).setValue(e,{emitEvent:!0}))}n&&(n.value="")}onValidate(){const e=this.createAlarmConfigForm.get("useMessageAlarmData").value,t=this.createAlarmConfigForm.get("overwriteAlarmDetails").value;if(!e||t){this.createAlarmConfigForm.get("scriptLang").value===d.JS&&this.jsFuncComponent.validateOnSubmit()}}}e("CreateAlarmConfigComponent",et),et.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:et,deps:[{token:G.Store},{token:E.UntypedFormBuilder},{token:J.NodeScriptTestService},{token:H.TranslateService}],target:t.ɵɵFactoryTarget.Component}),et.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:et,selector:"tb-action-node-create-alarm-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n \n {{ \'tb.rulenode.overwrite-alarm-details\' | translate }}\n \n
\n \n \n \n \n \n \n \n
\n \n
\n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n {{ \'tb.rulenode.use-alarm-severity-pattern\' | translate }}\n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n tb.rulenode.alarm-severity-pattern\n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n tb.rulenode.relation-types-list-hint\n \n
\n \n {{ \'tb.rulenode.propagate-to-owner\' | translate }}\n \n \n {{ \'tb.rulenode.propagate-to-tenant\' | translate }}\n \n
\n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ee.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:te.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"component",type:se.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:se.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:se.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:se.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:oe.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:me.SafePipe,name:"safe"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:et,decorators:[{type:n,args:[{selector:"tb-action-node-create-alarm-config",template:'
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n \n {{ \'tb.rulenode.overwrite-alarm-details\' | translate }}\n \n
\n \n \n \n \n \n \n \n
\n \n
\n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n {{ \'tb.rulenode.use-alarm-severity-pattern\' | translate }}\n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n tb.rulenode.alarm-severity-pattern\n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n tb.rulenode.relation-types-list-hint\n \n
\n \n {{ \'tb.rulenode.propagate-to-owner\' | translate }}\n \n \n {{ \'tb.rulenode.propagate-to-tenant\' | translate }}\n \n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder},{type:J.NodeScriptTestService},{type:H.TranslateService}]},propDecorators:{jsFuncComponent:[{type:o,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:o,args:["tbelFuncComponent",{static:!1}]}]}});class tt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(g),this.directionTypeTranslations=y,this.entityType=x}configForm(){return this.createRelationConfigForm}onConfigurationSet(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[D.required]],entityType:[e?e.entityType:null,[D.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[D.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[D.required,D.min(0)]]})}validatorTriggers(){return["entityType"]}updateValidators(e){const t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([D.required,D.pattern(/.*\S.*/)]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==x.DEVICE&&t!==x.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([D.required,D.pattern(/.*\S.*/)]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})}prepareOutputConfig(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e.entityTypePattern=e.entityTypePattern?e.entityTypePattern.trim():null,e}}e("CreateRelationConfigComponent",tt),tt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:tt,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),tt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:tt,selector:"tb-action-node-create-relation-config",usesInheritance:!0,ngImport:t,template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ue.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","filterAllowedEntityTypes","showLabel","required","disabled"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:tt,decorators:[{type:n,args:[{selector:"tb-action-node-create-relation-config",template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class nt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(g),this.directionTypeTranslations=y,this.entityType=x}configForm(){return this.deleteRelationConfigForm}onConfigurationSet(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[D.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[D.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[D.required,D.min(0)]]})}validatorTriggers(){return["deleteForSingleEntity","entityType"]}updateValidators(e){const t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,n=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([D.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&n?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([D.required,D.pattern(/.*\S.*/)]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})}prepareOutputConfig(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e}}e("DeleteRelationConfigComponent",nt),nt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:nt,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),nt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:nt,selector:"tb-action-node-delete-relation-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ue.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","filterAllowedEntityTypes","showLabel","required","disabled"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:nt,decorators:[{type:n,args:[{selector:"tb-action-node-delete-relation-config",template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class rt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.deviceProfile}onConfigurationSet(e){this.deviceProfile=this.fb.group({persistAlarmRulesState:[!!e&&e.persistAlarmRulesState,D.required],fetchAlarmRulesStateOnStart:[!!e&&e.fetchAlarmRulesStateOnStart,D.required]})}}e("DeviceProfileConfigComponent",rt),rt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:rt,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),rt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:rt,selector:"tb-device-profile-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.persist-alarm-rules\' | translate }}\n \n \n {{ \'tb.rulenode.fetch-alarm-rules\' | translate }}\n \n
\n',dependencies:[{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:rt,decorators:[{type:n,args:[{selector:"tb-device-profile-config",template:'
\n \n {{ \'tb.rulenode.persist-alarm-rules\' | translate }}\n \n \n {{ \'tb.rulenode.fetch-alarm-rules\' | translate }}\n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class ot extends s{constructor(e,t,n,o){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=o,this.tbelEnabled=$(this.store).tbelEnabled,this.scriptLanguage=d,this.changeScript=new r,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-generator-function",this.serviceType=p.TB_RULE_ENGINE}configForm(){return this.generatorConfigForm}onConfigurationSet(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[D.required,D.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[D.required,D.min(1)]],originator:[e?e.originator:null,[]],scriptLang:[e?e.scriptLang:d.JS,[D.required]],jsScript:[e?e.jsScript:null,[]],tbelScript:[e?e.tbelScript:null,[]],queueName:[e?e.queueName:null,[]]})}validatorTriggers(){return["scriptLang"]}updateValidators(e){let t=this.generatorConfigForm.get("scriptLang").value;t!==d.TBEL||this.tbelEnabled||(t=d.JS,this.generatorConfigForm.get("scriptLang").patchValue(t,{emitEvent:!1}),setTimeout((()=>{this.generatorConfigForm.updateValueAndValidity({emitEvent:!0})}))),this.generatorConfigForm.get("jsScript").setValidators(t===d.JS?[D.required]:[]),this.generatorConfigForm.get("jsScript").updateValueAndValidity({emitEvent:e}),this.generatorConfigForm.get("tbelScript").setValidators(t===d.TBEL?[D.required]:[]),this.generatorConfigForm.get("tbelScript").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=d.JS),e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e}prepareOutputConfig(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e}testScript(e){const t=this.generatorConfigForm.get("scriptLang").value,n=t===d.JS?"jsScript":"tbelScript",r=t===d.JS?"rulenode/generator_node_script_fn":"rulenode/tbel/generator_node_script_fn",o=this.generatorConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.generatorConfigForm.get(n).setValue(e),this.changeScript.emit())}))}onValidate(){this.generatorConfigForm.get("scriptLang").value===d.JS&&this.jsFuncComponent.validateOnSubmit()}}var at;e("GeneratorConfigComponent",ot),ot.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ot,deps:[{token:G.Store},{token:E.UntypedFormBuilder},{token:J.NodeScriptTestService},{token:H.TranslateService}],target:t.ɵɵFactoryTarget.Component}),ot.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:ot,selector:"tb-action-node-generator-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n\n \n \n\n \n \n \n \n \n \n \n
\n \n
\n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:pe.EntitySelectComponent,selector:"tb-entity-select",inputs:["allowedEntityTypes","useAliasEntityTypes","required","disabled"]},{kind:"component",type:Q.QueueAutocompleteComponent,selector:"tb-queue-autocomplete",inputs:["labelText","requiredText","autocompleteHint","subscriptSizing","required","queueType","disabled"]},{kind:"component",type:ee.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:te.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:oe.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ot,decorators:[{type:n,args:[{selector:"tb-action-node-generator-config",template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n\n \n \n\n \n \n \n \n \n \n \n
\n \n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder},{type:J.NodeScriptTestService},{type:H.TranslateService}]},propDecorators:{jsFuncComponent:[{type:o,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:o,args:["tbelFuncComponent",{static:!1}]}]}}),function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR",e.ENTITY="ENTITY"}(at||(at={}));const it=new Map([[at.CUSTOMER,"tb.rulenode.originator-customer"],[at.TENANT,"tb.rulenode.originator-tenant"],[at.RELATED,"tb.rulenode.originator-related"],[at.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"],[at.ENTITY,"tb.rulenode.originator-entity"]]);var lt;!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(lt||(lt={}));const st=new Map([[lt.CIRCLE,"tb.rulenode.perimeter-circle"],[lt.POLYGON,"tb.rulenode.perimeter-polygon"]]);var mt;!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(mt||(mt={}));const ut=new Map([[mt.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[mt.SECONDS,"tb.rulenode.time-unit-seconds"],[mt.MINUTES,"tb.rulenode.time-unit-minutes"],[mt.HOURS,"tb.rulenode.time-unit-hours"],[mt.DAYS,"tb.rulenode.time-unit-days"]]);var pt;!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(pt||(pt={}));const dt=new Map([[pt.METER,"tb.rulenode.range-unit-meter"],[pt.KILOMETER,"tb.rulenode.range-unit-kilometer"],[pt.FOOT,"tb.rulenode.range-unit-foot"],[pt.MILE,"tb.rulenode.range-unit-mile"],[pt.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);var ct;!function(e){e.ID="ID",e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.CITY="CITY",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(ct||(ct={}));const ft=new Map([[ct.ID,"tb.rulenode.entity-details-id"],[ct.TITLE,"tb.rulenode.entity-details-title"],[ct.COUNTRY,"tb.rulenode.entity-details-country"],[ct.STATE,"tb.rulenode.entity-details-state"],[ct.CITY,"tb.rulenode.entity-details-city"],[ct.ZIP,"tb.rulenode.entity-details-zip"],[ct.ADDRESS,"tb.rulenode.entity-details-address"],[ct.ADDRESS2,"tb.rulenode.entity-details-address2"],[ct.PHONE,"tb.rulenode.entity-details-phone"],[ct.EMAIL,"tb.rulenode.entity-details-email"],[ct.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);var gt;!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(gt||(gt={}));const yt=new Map([[gt.FIRST,"tb.rulenode.first"],[gt.LAST,"tb.rulenode.last"],[gt.ALL,"tb.rulenode.all"]]),xt=new Map([[gt.FIRST,"tb.rulenode.first-mode-hint"],[gt.LAST,"tb.rulenode.last-mode-hint"],[gt.ALL,"tb.rulenode.all-mode-hint"]]);var bt,ht;!function(e){e.ASC="ASC",e.DESC="DESC"}(bt||(bt={})),function(e){e.ATTRIBUTES="ATTRIBUTES",e.LATEST_TELEMETRY="LATEST_TELEMETRY",e.FIELDS="FIELDS"}(ht||(ht={}));const vt=new Map([[ht.ATTRIBUTES,"tb.rulenode.attributes"],[ht.LATEST_TELEMETRY,"tb.rulenode.latest-telemetry"],[ht.FIELDS,"tb.rulenode.fields"]]),Ct=new Map([[ht.ATTRIBUTES,"tb.rulenode.add-mapped-attribute-to"],[ht.LATEST_TELEMETRY,"tb.rulenode.add-mapped-latest-telemetry-to"],[ht.FIELDS,"tb.rulenode.add-mapped-fields-to"]]),Ft=new Map([[bt.ASC,"tb.rulenode.ascending"],[bt.DESC,"tb.rulenode.descending"]]);var kt;!function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(kt||(kt={}));const Lt=new Map([[kt.STANDARD,"tb.rulenode.sqs-queue-standard"],[kt.FIFO,"tb.rulenode.sqs-queue-fifo"]]),Tt=["anonymous","basic","cert.PEM"],It=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),Nt=["sas","cert.PEM"],St=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);var qt;!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(qt||(qt={}));const At=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],Mt=new Map([["US-ASCII","tb.rulenode.charset-us-ascii"],["ISO-8859-1","tb.rulenode.charset-iso-8859-1"],["UTF-8","tb.rulenode.charset-utf-8"],["UTF-16BE","tb.rulenode.charset-utf-16be"],["UTF-16LE","tb.rulenode.charset-utf-16le"],["UTF-16","tb.rulenode.charset-utf-16"]]);var Gt;!function(e){e.CUSTOM="CUSTOM",e.ADD="ADD",e.SUB="SUB",e.MULT="MULT",e.DIV="DIV",e.SIN="SIN",e.SINH="SINH",e.COS="COS",e.COSH="COSH",e.TAN="TAN",e.TANH="TANH",e.ACOS="ACOS",e.ASIN="ASIN",e.ATAN="ATAN",e.ATAN2="ATAN2",e.EXP="EXP",e.EXPM1="EXPM1",e.SQRT="SQRT",e.CBRT="CBRT",e.GET_EXP="GET_EXP",e.HYPOT="HYPOT",e.LOG="LOG",e.LOG10="LOG10",e.LOG1P="LOG1P",e.CEIL="CEIL",e.FLOOR="FLOOR",e.FLOOR_DIV="FLOOR_DIV",e.FLOOR_MOD="FLOOR_MOD",e.ABS="ABS",e.MIN="MIN",e.MAX="MAX",e.POW="POW",e.SIGNUM="SIGNUM",e.RAD="RAD",e.DEG="DEG"}(Gt||(Gt={}));const Et=new Map([[Gt.CUSTOM,{value:Gt.CUSTOM,name:"Custom Function",description:"Use this function to specify complex mathematical expression.",minArgs:1,maxArgs:16}],[Gt.ADD,{value:Gt.ADD,name:"Addition",description:"x + y",minArgs:2,maxArgs:2}],[Gt.SUB,{value:Gt.SUB,name:"Subtraction",description:"x - y",minArgs:2,maxArgs:2}],[Gt.MULT,{value:Gt.MULT,name:"Multiplication",description:"x * y",minArgs:2,maxArgs:2}],[Gt.DIV,{value:Gt.DIV,name:"Division",description:"x / y",minArgs:2,maxArgs:2}],[Gt.SIN,{value:Gt.SIN,name:"Sine",description:"Returns the trigonometric sine of an angle in radians.",minArgs:1,maxArgs:1}],[Gt.SINH,{value:Gt.SINH,name:"Hyperbolic sine",description:"Returns the hyperbolic sine of an argument.",minArgs:1,maxArgs:1}],[Gt.COS,{value:Gt.COS,name:"Cosine",description:"Returns the trigonometric cosine of an angle in radians.",minArgs:1,maxArgs:1}],[Gt.COSH,{value:Gt.COSH,name:"Hyperbolic cosine",description:"Returns the hyperbolic cosine of an argument.",minArgs:1,maxArgs:1}],[Gt.TAN,{value:Gt.TAN,name:"Tangent",description:"Returns the trigonometric tangent of an angle in radians",minArgs:1,maxArgs:1}],[Gt.TANH,{value:Gt.TANH,name:"Hyperbolic tangent",description:"Returns the hyperbolic tangent of an argument",minArgs:1,maxArgs:1}],[Gt.ACOS,{value:Gt.ACOS,name:"Arc cosine",description:"Returns the arc cosine of an argument",minArgs:1,maxArgs:1}],[Gt.ASIN,{value:Gt.ASIN,name:"Arc sine",description:"Returns the arc sine of an argument",minArgs:1,maxArgs:1}],[Gt.ATAN,{value:Gt.ATAN,name:"Arc tangent",description:"Returns the arc tangent of an argument",minArgs:1,maxArgs:1}],[Gt.ATAN2,{value:Gt.ATAN2,name:"2-argument arc tangent",description:"Returns the angle theta from the conversion of rectangular coordinates (x, y) to polar coordinates (r, theta)",minArgs:2,maxArgs:2}],[Gt.EXP,{value:Gt.EXP,name:"Exponential",description:"Returns Euler's number e raised to the power of an argument",minArgs:1,maxArgs:1}],[Gt.EXPM1,{value:Gt.EXPM1,name:"Exponential minus one",description:"Returns Euler's number e raised to the power of an argument minus one",minArgs:1,maxArgs:1}],[Gt.SQRT,{value:Gt.SQRT,name:"Square",description:"Returns the correctly rounded positive square root of an argument",minArgs:1,maxArgs:1}],[Gt.CBRT,{value:Gt.CBRT,name:"Cube root",description:"Returns the cube root of an argument",minArgs:1,maxArgs:1}],[Gt.GET_EXP,{value:Gt.GET_EXP,name:"Get exponent",description:"Returns the unbiased exponent used in the representation of an argument",minArgs:1,maxArgs:1}],[Gt.HYPOT,{value:Gt.HYPOT,name:"Square root",description:"Returns the square root of the squares of the arguments",minArgs:2,maxArgs:2}],[Gt.LOG,{value:Gt.LOG,name:"Logarithm",description:"Returns the natural logarithm of an argument",minArgs:1,maxArgs:1}],[Gt.LOG10,{value:Gt.LOG10,name:"Base 10 logarithm",description:"Returns the base 10 logarithm of an argument",minArgs:1,maxArgs:1}],[Gt.LOG1P,{value:Gt.LOG1P,name:"Logarithm of the sum",description:"Returns the natural logarithm of the sum of an argument",minArgs:1,maxArgs:1}],[Gt.CEIL,{value:Gt.CEIL,name:"Ceiling",description:"Returns the smallest (closest to negative infinity) of an argument",minArgs:1,maxArgs:1}],[Gt.FLOOR,{value:Gt.FLOOR,name:"Floor",description:"Returns the largest (closest to positive infinity) of an argument",minArgs:1,maxArgs:1}],[Gt.FLOOR_DIV,{value:Gt.FLOOR_DIV,name:"Floor division",description:"Returns the largest (closest to positive infinity) of the arguments",minArgs:2,maxArgs:2}],[Gt.FLOOR_MOD,{value:Gt.FLOOR_MOD,name:"Floor modulus",description:"Returns the floor modulus of the arguments",minArgs:2,maxArgs:2}],[Gt.ABS,{value:Gt.ABS,name:"Absolute",description:"Returns the absolute value of an argument",minArgs:1,maxArgs:1}],[Gt.MIN,{value:Gt.MIN,name:"Min",description:"Returns the smaller of the arguments",minArgs:2,maxArgs:2}],[Gt.MAX,{value:Gt.MAX,name:"Max",description:"Returns the greater of the arguments",minArgs:2,maxArgs:2}],[Gt.POW,{value:Gt.POW,name:"Raise to a power",description:"Returns the value of the first argument raised to the power of the second argument",minArgs:2,maxArgs:2}],[Gt.SIGNUM,{value:Gt.SIGNUM,name:"Sign of a real number",description:"Returns the signum function of the argument",minArgs:1,maxArgs:1}],[Gt.RAD,{value:Gt.RAD,name:"Radian",description:"Converts an angle measured in degrees to an approximately equivalent angle measured in radians",minArgs:1,maxArgs:1}],[Gt.DEG,{value:Gt.DEG,name:"Degrees",description:"Converts an angle measured in radians to an approximately equivalent angle measured in degrees.",minArgs:1,maxArgs:1}]]);var Dt,Vt,Pt;!function(e){e.MESSAGE_BODY="MESSAGE_BODY",e.MESSAGE_METADATA="MESSAGE_METADATA",e.ATTRIBUTE="ATTRIBUTE",e.TIME_SERIES="TIME_SERIES",e.CONSTANT="CONSTANT"}(Dt||(Dt={})),function(e){e.MESSAGE_BODY="MESSAGE_BODY",e.MESSAGE_METADATA="MESSAGE_METADATA",e.ATTRIBUTE="ATTRIBUTE",e.TIME_SERIES="TIME_SERIES"}(Vt||(Vt={})),function(e){e.DATA="DATA",e.METADATA="METADATA"}(Pt||(Pt={}));const wt=new Map([[Pt.DATA,"tb.rulenode.message"],[Pt.METADATA,"tb.rulenode.metadata"]]),Rt=new Map([[Dt.MESSAGE_BODY,{name:"tb.rulenode.message-body-type",description:"Fetch argument value from incoming message"}],[Dt.MESSAGE_METADATA,{name:"tb.rulenode.message-metadata-type",description:"Fetch argument value from incoming message metadata"}],[Dt.ATTRIBUTE,{name:"tb.rulenode.attribute-type",description:"Fetch attribute value from database"}],[Dt.TIME_SERIES,{name:"tb.rulenode.time-series-type",description:"Fetch latest time-series value from database"}],[Dt.CONSTANT,{name:"tb.rulenode.constant-type",description:"Define constant value"}]]),Ot=new Map([[Vt.MESSAGE_BODY,{name:"tb.rulenode.message-body-type",description:"Add result to the outgoing message"}],[Vt.MESSAGE_METADATA,{name:"tb.rulenode.message-metadata-type",description:"Add result to the outgoing message metadata"}],[Vt.ATTRIBUTE,{name:"tb.rulenode.attribute-type",description:"Store result as an entity attribute in the database"}],[Vt.TIME_SERIES,{name:"tb.rulenode.time-series-type",description:"Store result as an entity time-series in the database"}]]),Bt=["x","y","z","a","b","c","d","k","l","m","n","o","p","r","s","t"];var Kt,Ut;!function(e){e.SHARED_SCOPE="SHARED_SCOPE",e.SERVER_SCOPE="SERVER_SCOPE",e.CLIENT_SCOPE="CLIENT_SCOPE"}(Kt||(Kt={})),function(e){e.SHARED_SCOPE="SHARED_SCOPE",e.SERVER_SCOPE="SERVER_SCOPE"}(Ut||(Ut={}));const _t=new Map([[Kt.SHARED_SCOPE,"tb.rulenode.shared-scope"],[Kt.SERVER_SCOPE,"tb.rulenode.server-scope"],[Kt.CLIENT_SCOPE,"tb.rulenode.client-scope"]]);class Ht extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.perimeterType=lt,this.perimeterTypes=Object.keys(lt),this.perimeterTypeTranslationMap=st,this.rangeUnits=Object.keys(pt),this.rangeUnitTranslationMap=dt,this.timeUnits=Object.keys(mt),this.timeUnitsTranslationMap=ut}configForm(){return this.geoActionConfigForm}onConfigurationSet(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[D.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[D.required]],perimeterType:[e?e.perimeterType:null,[D.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterKeyName:[e?e.perimeterKeyName:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]],minInsideDuration:[e?e.minInsideDuration:null,[D.required,D.min(1),D.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[D.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[D.required,D.min(1),D.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[D.required]]})}validatorTriggers(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]}updateValidators(e){const t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,n=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterKeyName").setValidators([D.required]):this.geoActionConfigForm.get("perimeterKeyName").setValidators([]),t||n!==lt.CIRCLE?(this.geoActionConfigForm.get("centerLatitude").setValidators([]),this.geoActionConfigForm.get("centerLongitude").setValidators([]),this.geoActionConfigForm.get("range").setValidators([]),this.geoActionConfigForm.get("rangeUnit").setValidators([])):(this.geoActionConfigForm.get("centerLatitude").setValidators([D.required,D.min(-90),D.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([D.required,D.min(-180),D.max(180)]),this.geoActionConfigForm.get("range").setValidators([D.required,D.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([D.required])),t||n!==lt.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([D.required]),this.geoActionConfigForm.get("perimeterKeyName").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})}}e("GpsGeoActionConfigComponent",Ht),Ht.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ht,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ht.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Ht,selector:"tb-action-node-gps-geofencing-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-metadata\' | translate }}\n \n \n tb.rulenode.perimeter-key-name\n \n \n {{ \'tb.rulenode.perimeter-key-name-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ht,decorators:[{type:n,args:[{selector:"tb-action-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-metadata\' | translate }}\n \n \n tb.rulenode.perimeter-key-name\n \n \n {{ \'tb.rulenode.perimeter-key-name-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class zt extends s{constructor(e,t,n,o){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=o,this.tbelEnabled=$(this.store).tbelEnabled,this.scriptLanguage=d,this.changeScript=new r,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-to-string-function"}configForm(){return this.logConfigForm}onConfigurationSet(e){this.logConfigForm=this.fb.group({scriptLang:[e?e.scriptLang:d.JS,[D.required]],jsScript:[e?e.jsScript:null,[]],tbelScript:[e?e.tbelScript:null,[]]})}validatorTriggers(){return["scriptLang"]}updateValidators(e){let t=this.logConfigForm.get("scriptLang").value;t!==d.TBEL||this.tbelEnabled||(t=d.JS,this.logConfigForm.get("scriptLang").patchValue(t,{emitEvent:!1}),setTimeout((()=>{this.logConfigForm.updateValueAndValidity({emitEvent:!0})}))),this.logConfigForm.get("jsScript").setValidators(t===d.JS?[D.required]:[]),this.logConfigForm.get("jsScript").updateValueAndValidity({emitEvent:e}),this.logConfigForm.get("tbelScript").setValidators(t===d.TBEL?[D.required]:[]),this.logConfigForm.get("tbelScript").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=d.JS)),e}testScript(e){const t=this.logConfigForm.get("scriptLang").value,n=t===d.JS?"jsScript":"tbelScript",r=t===d.JS?"rulenode/log_node_script_fn":"rulenode/tbel/log_node_script_fn",o=this.logConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.logConfigForm.get(n).setValue(e),this.changeScript.emit())}))}onValidate(){this.logConfigForm.get("scriptLang").value===d.JS&&this.jsFuncComponent.validateOnSubmit()}}e("LogConfigComponent",zt),zt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:zt,deps:[{token:G.Store},{token:E.UntypedFormBuilder},{token:J.NodeScriptTestService},{token:H.TranslateService}],target:t.ɵɵFactoryTarget.Component}),zt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:zt,selector:"tb-action-node-log-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n \n \n
\n \n
\n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ee.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:te.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:oe.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:zt,decorators:[{type:n,args:[{selector:"tb-action-node-log-config",template:'
\n \n \n \n \n \n \n \n
\n \n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder},{type:J.NodeScriptTestService},{type:H.TranslateService}]},propDecorators:{jsFuncComponent:[{type:o,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:o,args:["tbelFuncComponent",{static:!1}]}]}});class jt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.msgCountConfigForm}onConfigurationSet(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[D.required,D.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[D.required]]})}}e("MsgCountConfigComponent",jt),jt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:jt,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),jt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:jt,selector:"tb-action-node-msg-count-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:jt,decorators:[{type:n,args:[{selector:"tb-action-node-msg-count-config",template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Qt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.msgDelayConfigForm}onConfigurationSet(e){this.msgDelayConfigForm=this.fb.group({useMetadataPeriodInSecondsPatterns:[!!e&&e.useMetadataPeriodInSecondsPatterns,[]],periodInSeconds:[e?e.periodInSeconds:null,[]],periodInSecondsPattern:[e?e.periodInSecondsPattern:null,[]],maxPendingMsgs:[e?e.maxPendingMsgs:null,[D.required,D.min(1),D.max(1e5)]]})}validatorTriggers(){return["useMetadataPeriodInSecondsPatterns"]}updateValidators(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([D.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([D.required,D.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})}}e("MsgDelayConfigComponent",Qt),Qt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Qt,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Qt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Qt,selector:"tb-action-node-msg-delay-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Qt,decorators:[{type:n,args:[{selector:"tb-action-node-msg-delay-config",template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class $t extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopes=Object.keys(m),this.telemetryTypeTranslationsMap=u}configForm(){return this.pushToCloudConfigForm}onConfigurationSet(e){this.pushToCloudConfigForm=this.fb.group({scope:[e?e.scope:null,[D.required]]})}}e("PushToCloudConfigComponent",$t),$t.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:$t,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),$t.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:$t,selector:"tb-action-node-push-to-cloud-config",usesInheritance:!0,ngImport:t,template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:$t,decorators:[{type:n,args:[{selector:"tb-action-node-push-to-cloud-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Jt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopes=Object.keys(m),this.telemetryTypeTranslationsMap=u}configForm(){return this.pushToEdgeConfigForm}onConfigurationSet(e){this.pushToEdgeConfigForm=this.fb.group({scope:[e?e.scope:null,[D.required]]})}}e("PushToEdgeConfigComponent",Jt),Jt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Jt,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Jt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Jt,selector:"tb-action-node-push-to-edge-config",usesInheritance:!0,ngImport:t,template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Jt,decorators:[{type:n,args:[{selector:"tb-action-node-push-to-edge-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Yt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.rpcReplyConfigForm}onConfigurationSet(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})}}e("RpcReplyConfigComponent",Yt),Yt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Yt,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Yt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Yt,selector:"tb-action-node-rpc-reply-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n',dependencies:[{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Yt,decorators:[{type:n,args:[{selector:"tb-action-node-rpc-reply-config",template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Wt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.rpcRequestConfigForm}onConfigurationSet(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[D.required,D.min(0)]]})}}e("RpcRequestConfigComponent",Wt),Wt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Wt,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Wt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Wt,selector:"tb-action-node-rpc-request-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Wt,decorators:[{type:n,args:[{selector:"tb-action-node-rpc-request-config",template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Xt extends b{get required(){return this.requiredValue}set required(e){this.requiredValue=de(e)}constructor(e,t,n,r){super(e),this.store=e,this.translate=t,this.injector=n,this.fb=r,this.propagateChange=null,this.valueChangeSubscription=null}ngOnInit(){this.ngControl=this.injector.get(V),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))}keyValsFormArray(){return this.kvListFormGroup.get("keyVals")}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})}writeValue(e){this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();const t=[];if(e)for(const n of Object.keys(e))Object.prototype.hasOwnProperty.call(e,n)&&t.push(this.fb.group({key:[n,[D.required]],value:[e[n],[D.required]]}));this.kvListFormGroup.setControl("keyVals",this.fb.array(t)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((()=>{this.updateModel()}))}removeKeyVal(e){this.kvListFormGroup.get("keyVals").removeAt(e)}addKeyVal(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[D.required]],value:["",[D.required]]}))}validate(e){const t=this.kvListFormGroup.get("keyVals").value;if(!t.length&&this.required)return{kvMapRequired:!0};if(!this.kvListFormGroup.valid)return{kvFieldsRequired:!0};if(this.uniqueKeyValuePairValidator)for(const e of t)if(e.key===e.value)return{uniqueKeyValuePair:!0};return null}updateModel(){const e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{const t={};e.forEach((e=>{t[e.key]=e.value})),this.propagateChange(t)}}}e("KvMapConfigOldComponent",Xt),Xt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Xt,deps:[{token:G.Store},{token:H.TranslateService},{token:t.Injector},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Xt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Xt,selector:"tb-kv-map-config-old",inputs:{disabled:"disabled",uniqueKeyValuePairValidator:"uniqueKeyValuePairValidator",requiredText:"requiredText",keyText:"keyText",keyRequiredText:"keyRequiredText",valText:"valText",valRequiredText:"valRequiredText",hintText:"hintText",required:"required"},providers:[{provide:P,useExisting:a((()=>Xt)),multi:!0},{provide:w,useExisting:a((()=>Xt)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n
\n {{ keyText | translate }}\n {{ valText | translate }}\n \n
\n
\n
\n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n
\n \n
\n \n
\n
\n',styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:#757575;font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:0;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host .tb-kv-map-config tb-error{display:block;margin-top:-12px}\n"],dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ce.TbErrorComponent,selector:"tb-error",inputs:["noMargin","error"]},{kind:"component",type:te.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:fe.DefaultShowHideDirective,selector:" [fxShow], [fxShow.print], [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl], [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl], [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg], [fxHide], [fxHide.print], [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl], [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl], [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg]",inputs:["fxShow","fxShow.print","fxShow.xs","fxShow.sm","fxShow.md","fxShow.lg","fxShow.xl","fxShow.lt-sm","fxShow.lt-md","fxShow.lt-lg","fxShow.lt-xl","fxShow.gt-xs","fxShow.gt-sm","fxShow.gt-md","fxShow.gt-lg","fxHide","fxHide.print","fxHide.xs","fxHide.sm","fxHide.md","fxHide.lg","fxHide.xl","fxHide.lt-sm","fxHide.lt-md","fxHide.lt-lg","fxHide.lt-xl","fxHide.gt-xs","fxHide.gt-sm","fxHide.gt-md","fxHide.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormControlDirective,selector:"[formControl]",inputs:["formControl","disabled","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormArrayName,selector:"[formArrayName]",inputs:["formArrayName"]},{kind:"pipe",type:R.AsyncPipe,name:"async"},{kind:"pipe",type:me.SafePipe,name:"safe"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Xt,decorators:[{type:n,args:[{selector:"tb-kv-map-config-old",providers:[{provide:P,useExisting:a((()=>Xt)),multi:!0},{provide:w,useExisting:a((()=>Xt)),multi:!0}],template:'
\n
\n {{ keyText | translate }}\n {{ valText | translate }}\n \n
\n
\n
\n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n
\n \n
\n \n
\n
\n',styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:#757575;font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:0;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host .tb-kv-map-config tb-error{display:block;margin-top:-12px}\n"]}]}],ctorParameters:function(){return[{type:G.Store},{type:H.TranslateService},{type:t.Injector},{type:E.FormBuilder}]},propDecorators:{disabled:[{type:i}],uniqueKeyValuePairValidator:[{type:i}],requiredText:[{type:i}],keyText:[{type:i}],keyRequiredText:[{type:i}],valText:[{type:i}],valRequiredText:[{type:i}],hintText:[{type:i}],required:[{type:i}]}});class Zt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.saveToCustomTableConfigForm}onConfigurationSet(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[D.required,D.pattern(/.*\S.*/)]],fieldsMapping:[e?e.fieldsMapping:null,[D.required]]})}prepareOutputConfig(e){return e.tableName=e.tableName.trim(),e}}e("SaveToCustomTableConfigComponent",Zt),Zt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Zt,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Zt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Zt,selector:"tb-action-node-custom-table-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n tb.rulenode.custom-table-hint\n \n \n \n \n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Zt,decorators:[{type:n,args:[{selector:"tb-action-node-custom-table-config",template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n tb.rulenode.custom-table-hint\n \n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class en extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.timeseriesConfigForm}onConfigurationSet(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[D.required,D.min(0)]],skipLatestPersistence:[!!e&&e.skipLatestPersistence,[]],useServerTs:[!!e&&e.useServerTs,[]]})}}e("TimeseriesConfigComponent",en),en.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:en,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),en.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:en,selector:"tb-action-node-timeseries-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.skip-latest-persistence\' | translate }}\n \n \n {{ \'tb.rulenode.use-server-ts\' | translate }}\n \n
tb.rulenode.use-server-ts-hint
\n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:en,decorators:[{type:n,args:[{selector:"tb-action-node-timeseries-config",template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.skip-latest-persistence\' | translate }}\n \n \n {{ \'tb.rulenode.use-server-ts\' | translate }}\n \n
tb.rulenode.use-server-ts-hint
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class tn extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.unassignCustomerConfigForm}onConfigurationSet(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[D.required,D.pattern(/.*\S.*/)]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[D.required,D.min(0)]]})}prepareOutputConfig(e){return e.customerNamePattern=e.customerNamePattern.trim(),e}}e("UnassignCustomerConfigComponent",tn),tn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:tn,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),tn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:tn,selector:"tb-action-node-un-assign-to-customer-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:tn,decorators:[{type:n,args:[{selector:"tb-action-node-un-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class nn extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopeMap=m,this.attributeScopes=Object.keys(m),this.telemetryTypeTranslationsMap=u,this.separatorKeysCodes=[ae,ie,le]}configForm(){return this.deleteAttributesConfigForm}onConfigurationSet(e){this.deleteAttributesConfigForm=this.fb.group({scope:[e?e.scope:null,[D.required]],keys:[e?e.keys:null,[D.required]],sendAttributesDeletedNotification:[!!e&&e.sendAttributesDeletedNotification,[]],notifyDevice:[!!e&&e.notifyDevice,[]]}),this.deleteAttributesConfigForm.get("scope").valueChanges.subscribe((e=>{e!==m.SHARED_SCOPE&&this.deleteAttributesConfigForm.get("notifyDevice").patchValue(!1,{emitEvent:!1})}))}removeKey(e){const t=this.deleteAttributesConfigForm.get("keys").value,n=t.indexOf(e);n>=0&&(t.splice(n,1),this.deleteAttributesConfigForm.get("keys").patchValue(t,{emitEvent:!0}))}addKey(e){const t=e.input;let n=e.value;if((n||"").trim()){n=n.trim();let e=this.deleteAttributesConfigForm.get("keys").value;e&&-1!==e.indexOf(n)||(e||(e=[]),e.push(n),this.deleteAttributesConfigForm.get("keys").patchValue(e,{emitEvent:!0}))}t&&(t.value="")}}e("DeleteAttributesConfigComponent",nn),nn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:nn,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),nn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:nn,selector:"tb-action-node-delete-attributes-config",viewQueries:[{propertyName:"attributeChipList",first:!0,predicate:["attributeChipList"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'attribute.attributes-scope\' | translate }}\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n \n \n \n {{key}}\n close\n \n \n \n {{ \'tb.rulenode.attributes-keys-required\' | translate }}\n tb.rulenode.general-pattern-hint\n \n \n {{ \'tb.rulenode.send-attributes-deleted-notification\' | translate }}\n \n
tb.rulenode.send-attributes-deleted-notification-hint
\n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-delete-hint
\n
\n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:se.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:se.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:se.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:se.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:nn,decorators:[{type:n,args:[{selector:"tb-action-node-delete-attributes-config",template:'
\n \n {{ \'attribute.attributes-scope\' | translate }}\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n \n \n \n {{key}}\n close\n \n \n \n {{ \'tb.rulenode.attributes-keys-required\' | translate }}\n tb.rulenode.general-pattern-hint\n \n \n {{ \'tb.rulenode.send-attributes-deleted-notification\' | translate }}\n \n
tb.rulenode.send-attributes-deleted-notification-hint
\n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-delete-hint
\n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]},propDecorators:{attributeChipList:[{type:o,args:["attributeChipList"]}]}});class rn extends b{get function(){return this.functionValue}set function(e){e&&this.functionValue!==e&&(this.functionValue=e,this.setupArgumentsFormGroup(!0))}constructor(e,t,n,r){super(e),this.store=e,this.translate=t,this.injector=n,this.fb=r,this.maxArgs=16,this.minArgs=1,this.displayArgumentName=!1,this.mathFunctionMap=Et,this.ArgumentType=Dt,this.attributeScopeMap=_t,this.argumentTypeMap=Rt,this.arguments=Object.values(Dt),this.attributeScope=Object.values(Kt),this.propagateChange=null,this.valueChangeSubscription=[]}ngOnInit(){this.ngControl=this.injector.get(V),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.argumentsFormGroup=this.fb.group({arguments:this.fb.array([])}),this.valueChangeSubscription.push(this.argumentsFormGroup.valueChanges.subscribe((()=>{this.updateModel()}))),this.setupArgumentsFormGroup()}onDrop(e){const t=this.argumentsFormArray(),n=t.at(e.previousIndex);t.removeAt(e.previousIndex),t.insert(e.currentIndex,n),this.updateArgumentNames()}argumentsFormArray(){return this.argumentsFormGroup.get("arguments")}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.argumentsFormGroup.disable({emitEvent:!1}):(this.argumentsFormGroup.enable({emitEvent:!1}),this.argumentsFormGroup.get("arguments").controls.forEach((e=>this.updateArgumentControlValidators(e))))}ngOnDestroy(){this.valueChangeSubscription.length&&this.valueChangeSubscription.forEach((e=>e.unsubscribe()))}writeValue(e){const t=[];e&&e.forEach(((e,n)=>{t.push(this.createArgumentControl(e,n))})),this.argumentsFormGroup.setControl("arguments",this.fb.array(t),{emitEvent:!1}),this.setupArgumentsFormGroup()}removeArgument(e){this.argumentsFormGroup.get("arguments").removeAt(e),this.updateArgumentNames()}addArgument(e=!0){const t=this.argumentsFormGroup.get("arguments"),n=this.createArgumentControl(null,t.length);t.push(n,{emitEvent:e})}validate(e){return this.argumentsFormGroup.valid?null:{argumentsRequired:!0}}setupArgumentsFormGroup(e=!1){if(this.function&&(this.maxArgs=this.mathFunctionMap.get(this.function).maxArgs,this.minArgs=this.mathFunctionMap.get(this.function).minArgs,this.displayArgumentName=this.function===Gt.CUSTOM),this.argumentsFormGroup){for(this.argumentsFormGroup.get("arguments").setValidators([D.minLength(this.minArgs),D.maxLength(this.maxArgs)]),this.argumentsFormGroup.get("arguments").value.length>this.maxArgs&&(this.argumentsFormGroup.get("arguments").controls.length=this.maxArgs);this.argumentsFormGroup.get("arguments").value.length{this.updateArgumentControlValidators(n),n.get("attributeScope").updateValueAndValidity({emitEvent:!1}),n.get("defaultValue").updateValueAndValidity({emitEvent:!1})}))),n}updateArgumentControlValidators(e){const t=e.get("type").value;t===Dt.ATTRIBUTE?e.get("attributeScope").enable({emitEvent:!1}):e.get("attributeScope").disable({emitEvent:!1}),t&&t!==Dt.CONSTANT?e.get("defaultValue").enable({emitEvent:!1}):e.get("defaultValue").disable({emitEvent:!1})}updateArgumentNames(){this.argumentsFormGroup.get("arguments").controls.forEach(((e,t)=>{e.get("name").setValue(Bt[t])}))}updateModel(){const e=this.argumentsFormGroup.get("arguments").value;e.length&&this.argumentsFormGroup.valid?this.propagateChange(e):this.propagateChange(null)}}e("ArgumentsMapConfigComponent",rn),rn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:rn,deps:[{token:G.Store},{token:H.TranslateService},{token:t.Injector},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),rn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:rn,selector:"tb-arguments-map-config",inputs:{disabled:"disabled",function:"function"},providers:[{provide:P,useExisting:a((()=>rn)),multi:!0},{provide:w,useExisting:a((()=>rn)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n\n
\n \n \n
\n \n
\n {{argumentControl.get(\'name\').value}}.\n
\n \n tb.rulenode.argument-source-field-input\n \n \n {{ argumentTypeMap.get(argumentControl.get(\'type\').value)?.name | translate }}\n \n \n {{ argumentTypeMap.get(argument).name | translate }}\n \n {{ argumentTypeMap.get(argument).description }}\n \n \n \n \n tb.rulenode.argument-source-field-input-required\n \n \n
\n \n tb.rulenode.argument-key-field-input\n \n \n help\n \n \n tb.rulenode.argument-key-field-input-required\n \n \n \n tb.rulenode.constant-value-field-input\n \n \n tb.rulenode.constant-value-field-input-required\n \n \n \n tb.rulenode.default-value-field-input\n \n \n
\n \n tb.rulenode.attribute-scope-field-input\n \n \n {{ attributeScopeMap.get(scope) | translate }}\n \n \n \n tb.rulenode.attribute-scope-field-input-required\n \n \n
\n \n
\n
\n
\n
\n
\n
\n tb.rulenode.no-arguments-prompt\n
\n \n
\n',styles:[":host .mat-mdc-list-item.tb-argument{border:solid rgba(0,0,0,.25) 1px;border-radius:4px;padding:10px 0;margin-bottom:16px}:host .arguments-list{padding:0}\n"],dependencies:[{kind:"directive",type:R.NgClass,selector:"[ngClass]",inputs:["class","ngClass"]},{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:te.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"directive",type:z.MatSelectTrigger,selector:"mat-select-trigger"},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"component",type:ge.MatList,selector:"mat-list",exportAs:["matList"]},{kind:"component",type:ge.MatListItem,selector:"mat-list-item, a[mat-list-item], button[mat-list-item]",inputs:["activated"],exportAs:["matListItem"]},{kind:"directive",type:ye.CdkDropList,selector:"[cdkDropList], cdk-drop-list",inputs:["cdkDropListConnectedTo","cdkDropListData","cdkDropListOrientation","id","cdkDropListLockAxis","cdkDropListDisabled","cdkDropListSortingDisabled","cdkDropListEnterPredicate","cdkDropListSortPredicate","cdkDropListAutoScrollDisabled","cdkDropListAutoScrollStep"],outputs:["cdkDropListDropped","cdkDropListEntered","cdkDropListExited","cdkDropListSorted"],exportAs:["cdkDropList"]},{kind:"directive",type:ye.CdkDrag,selector:"[cdkDrag]",inputs:["cdkDragData","cdkDragLockAxis","cdkDragRootElement","cdkDragBoundary","cdkDragStartDelay","cdkDragFreeDragPosition","cdkDragDisabled","cdkDragConstrainPosition","cdkDragPreviewClass","cdkDragPreviewContainer"],outputs:["cdkDragStarted","cdkDragReleased","cdkDragEnded","cdkDragEntered","cdkDragExited","cdkDragDropped","cdkDragMoved"],exportAs:["cdkDrag"]},{kind:"directive",type:ye.CdkDragHandle,selector:"[cdkDragHandle]",inputs:["cdkDragHandleDisabled"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:fe.DefaultClassDirective,selector:" [ngClass], [ngClass.xs], [ngClass.sm], [ngClass.md], [ngClass.lg], [ngClass.xl], [ngClass.lt-sm], [ngClass.lt-md], [ngClass.lt-lg], [ngClass.lt-xl], [ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg]",inputs:["ngClass","ngClass.xs","ngClass.sm","ngClass.md","ngClass.lg","ngClass.xl","ngClass.lt-sm","ngClass.lt-md","ngClass.lt-lg","ngClass.lt-xl","ngClass.gt-xs","ngClass.gt-sm","ngClass.gt-md","ngClass.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormControlDirective,selector:"[formControl]",inputs:["formControl","disabled","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormArrayName,selector:"[formArrayName]",inputs:["formArrayName"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:rn,decorators:[{type:n,args:[{selector:"tb-arguments-map-config",providers:[{provide:P,useExisting:a((()=>rn)),multi:!0},{provide:w,useExisting:a((()=>rn)),multi:!0}],template:'
\n\n
\n \n \n
\n \n
\n {{argumentControl.get(\'name\').value}}.\n
\n \n tb.rulenode.argument-source-field-input\n \n \n {{ argumentTypeMap.get(argumentControl.get(\'type\').value)?.name | translate }}\n \n \n {{ argumentTypeMap.get(argument).name | translate }}\n \n {{ argumentTypeMap.get(argument).description }}\n \n \n \n \n tb.rulenode.argument-source-field-input-required\n \n \n
\n \n tb.rulenode.argument-key-field-input\n \n \n help\n \n \n tb.rulenode.argument-key-field-input-required\n \n \n \n tb.rulenode.constant-value-field-input\n \n \n tb.rulenode.constant-value-field-input-required\n \n \n \n tb.rulenode.default-value-field-input\n \n \n
\n \n tb.rulenode.attribute-scope-field-input\n \n \n {{ attributeScopeMap.get(scope) | translate }}\n \n \n \n tb.rulenode.attribute-scope-field-input-required\n \n \n
\n \n
\n
\n
\n
\n
\n
\n tb.rulenode.no-arguments-prompt\n
\n \n
\n',styles:[":host .mat-mdc-list-item.tb-argument{border:solid rgba(0,0,0,.25) 1px;border-radius:4px;padding:10px 0;margin-bottom:16px}:host .arguments-list{padding:0}\n"]}]}],ctorParameters:function(){return[{type:G.Store},{type:H.TranslateService},{type:t.Injector},{type:E.FormBuilder}]},propDecorators:{disabled:[{type:i}],function:[{type:i}]}});class on extends b{get required(){return this.requiredValue}set required(e){this.requiredValue=de(e)}constructor(e,t,n,r){super(e),this.store=e,this.translate=t,this.injector=n,this.fb=r,this.searchText="",this.dirty=!1,this.mathOperation=[...Et.values()],this.propagateChange=null}ngOnInit(){this.mathFunctionForm=this.fb.group({operation:[""]}),this.filteredOptions=this.mathFunctionForm.get("operation").valueChanges.pipe(xe((e=>{let t;t="string"==typeof e&&Gt[e]?Gt[e]:null,this.updateView(t)})),be((e=>(this.searchText=e||"",e?this._filter(e):this.mathOperation.slice()))))}_filter(e){const t=e.toLowerCase();return this.mathOperation.filter((e=>e.name.toLowerCase().includes(t)||e.value.toLowerCase().includes(t)))}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.mathFunctionForm.disable({emitEvent:!1}):this.mathFunctionForm.enable({emitEvent:!1})}mathFunctionDisplayFn(e){if(e){const t=Et.get(e);return t.value+" | "+t.name}return""}writeValue(e){this.modelValue=e,this.mathFunctionForm.get("operation").setValue(e,{emitEvent:!1}),this.dirty=!0}updateView(e){this.modelValue!==e&&(this.modelValue=e,this.propagateChange(this.modelValue))}onFocus(){this.dirty&&(this.mathFunctionForm.get("operation").updateValueAndValidity({onlySelf:!0}),this.dirty=!1)}clear(){this.mathFunctionForm.get("operation").patchValue(""),setTimeout((()=>{this.operationInput.nativeElement.blur(),this.operationInput.nativeElement.focus()}),0)}}e("MathFunctionAutocompleteComponent",on),on.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:on,deps:[{token:G.Store},{token:H.TranslateService},{token:t.Injector},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),on.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:on,selector:"tb-math-function-autocomplete",inputs:{required:"required",disabled:"disabled"},providers:[{provide:P,useExisting:a((()=>on)),multi:!0}],viewQueries:[{propertyName:"operationInput",first:!0,predicate:["operationInput"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'\n tb.rulenode.functions-field-input\n \n \n \n \n \n \n {{ option.description }}\n \n \n \n tb.rulenode.no-option-found\n \n \n\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:ke.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple","hideSingleSelectionIndicator"],exportAs:["matAutocomplete"]},{kind:"directive",type:ke.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:R.AsyncPipe,name:"async"},{kind:"pipe",type:Le.HighlightPipe,name:"highlight"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:on,decorators:[{type:n,args:[{selector:"tb-math-function-autocomplete",providers:[{provide:P,useExisting:a((()=>on)),multi:!0}],template:'\n tb.rulenode.functions-field-input\n \n \n \n \n \n \n {{ option.description }}\n \n \n \n tb.rulenode.no-option-found\n \n \n\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:H.TranslateService},{type:t.Injector},{type:E.UntypedFormBuilder}]},propDecorators:{required:[{type:i}],disabled:[{type:i}],operationInput:[{type:o,args:["operationInput",{static:!0}]}]}});class an extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.MathFunction=Gt,this.ArgumentTypeResult=Vt,this.argumentTypeResultMap=Ot,this.attributeScopeMap=_t,this.argumentsResult=Object.values(Vt),this.attributeScopeResult=Object.values(Ut)}configForm(){return this.mathFunctionConfigForm}onConfigurationSet(e){this.mathFunctionConfigForm=this.fb.group({operation:[e?e.operation:null,[D.required]],arguments:[e?e.arguments:null,[D.required]],customFunction:[e?e.customFunction:"",[D.required]],result:this.fb.group({type:[e?e.result.type:null,[D.required]],attributeScope:[e?e.result.attributeScope:null,[D.required]],key:[e?e.result.key:"",[D.required]],resultValuePrecision:[e?e.result.resultValuePrecision:0],addToBody:[!!e&&e.result.addToBody],addToMetadata:[!!e&&e.result.addToMetadata]})})}updateValidators(e){const t=this.mathFunctionConfigForm.get("operation").value,n=this.mathFunctionConfigForm.get("result.type").value;t===Gt.CUSTOM?this.mathFunctionConfigForm.get("customFunction").enable({emitEvent:!1}):this.mathFunctionConfigForm.get("customFunction").disable({emitEvent:!1}),n===Vt.ATTRIBUTE?this.mathFunctionConfigForm.get("result.attributeScope").enable({emitEvent:!1}):this.mathFunctionConfigForm.get("result.attributeScope").disable({emitEvent:!1}),this.mathFunctionConfigForm.get("customFunction").updateValueAndValidity({emitEvent:e}),this.mathFunctionConfigForm.get("result.attributeScope").updateValueAndValidity({emitEvent:e})}validatorTriggers(){return["operation","result.type"]}}e("MathFunctionConfigComponent",an),an.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:an,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),an.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:an,selector:"tb-action-node-math-function-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n tb.rulenode.argument-tile\n \n \n
\n
\n {{\'tb.rulenode.custom-expression-field-input\' | translate }} *\n \n \n \n tb.rulenode.custom-expression-field-input-required\n \n tb.rulenode.custom-expression-field-input-hint\n \n
\n
\n tb.rulenode.result-title\n
\n \n tb.rulenode.type-field-input\n \n \n {{ argumentTypeResultMap.get(mathFunctionConfigForm.get(\'result.type\').value)?.name | translate }}\n \n \n {{ argumentTypeResultMap.get(argument).name | translate }}\n \n {{ argumentTypeResultMap.get(argument).description }}\n \n \n \n \n tb.rulenode.type-field-input-required\n \n \n
\n \n tb.rulenode.attribute-scope-field-input\n \n \n {{ attributeScopeMap.get(scope) | translate }}\n \n \n \n \n tb.rulenode.key-field-input\n \n help\n \n tb.rulenode.key-field-input-required\n \n \n
\n
\n \n tb.rulenode.number-floating-point-field-input\n \n \n \n
\n
\n \n {{\'tb.rulenode.add-to-message-field-input\' | translate }}\n \n \n {{\'tb.rulenode.add-to-metadata-field-input\' | translate}}\n \n
\n
\n
\n
\n',styles:[":host ::ng-deep .fields-group{padding:0 16px 8px;margin:10px 0;border:1px groove rgba(0,0,0,.25);border-radius:4px}:host ::ng-deep .fields-group .mat-mdc-form-field .mat-mdc-form-field-infix{width:100%}:host ::ng-deep .fields-group legend{color:#000000b3;width:-moz-fit-content;width:fit-content}:host ::ng-deep .fields-group legend+*{display:block}:host ::ng-deep .fields-group legend+*.no-margin-top{margin-top:0}\n"],dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"directive",type:z.MatSelectTrigger,selector:"mat-select-trigger"},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:E.FormGroupName,selector:"[formGroupName]",inputs:["formGroupName"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:rn,selector:"tb-arguments-map-config",inputs:["disabled","function"]},{kind:"component",type:on,selector:"tb-math-function-autocomplete",inputs:["required","disabled"]},{kind:"pipe",type:me.SafePipe,name:"safe"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:an,decorators:[{type:n,args:[{selector:"tb-action-node-math-function-config",template:'
\n \n \n
\n tb.rulenode.argument-tile\n \n \n
\n
\n {{\'tb.rulenode.custom-expression-field-input\' | translate }} *\n \n \n \n tb.rulenode.custom-expression-field-input-required\n \n tb.rulenode.custom-expression-field-input-hint\n \n
\n
\n tb.rulenode.result-title\n
\n \n tb.rulenode.type-field-input\n \n \n {{ argumentTypeResultMap.get(mathFunctionConfigForm.get(\'result.type\').value)?.name | translate }}\n \n \n {{ argumentTypeResultMap.get(argument).name | translate }}\n \n {{ argumentTypeResultMap.get(argument).description }}\n \n \n \n \n tb.rulenode.type-field-input-required\n \n \n
\n \n tb.rulenode.attribute-scope-field-input\n \n \n {{ attributeScopeMap.get(scope) | translate }}\n \n \n \n \n tb.rulenode.key-field-input\n \n help\n \n tb.rulenode.key-field-input-required\n \n \n
\n
\n \n tb.rulenode.number-floating-point-field-input\n \n \n \n
\n
\n \n {{\'tb.rulenode.add-to-message-field-input\' | translate }}\n \n \n {{\'tb.rulenode.add-to-metadata-field-input\' | translate}}\n \n
\n
\n
\n
\n',styles:[":host ::ng-deep .fields-group{padding:0 16px 8px;margin:10px 0;border:1px groove rgba(0,0,0,.25);border-radius:4px}:host ::ng-deep .fields-group .mat-mdc-form-field .mat-mdc-form-field-infix{width:100%}:host ::ng-deep .fields-group legend{color:#000000b3;width:-moz-fit-content;width:fit-content}:host ::ng-deep .fields-group legend+*{display:block}:host ::ng-deep .fields-group legend+*.no-margin-top{margin-top:0}\n"]}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class ln extends b{set required(e){this.requiredValue!==e&&(this.requiredValue=e,this.updateValidators())}get required(){return this.requiredValue}constructor(e,t){super(e),this.store=e,this.fb=t,this.subscriptSizing="fixed",this.searchText="",this.dirty=!1,this.messageTypes=["POST_ATTRIBUTES_REQUEST","POST_TELEMETRY_REQUEST"],this.propagateChange=e=>{},this.messageTypeFormGroup=this.fb.group({messageType:[null,[D.maxLength(255)]]})}registerOnTouched(e){}registerOnChange(e){this.propagateChange=e}ngOnInit(){this.outputMessageTypes=this.messageTypeFormGroup.get("messageType").valueChanges.pipe(xe((e=>{this.updateView(e)})),be((e=>e||"")),he((e=>this.fetchMessageTypes(e))))}writeValue(e){this.searchText="",this.modelValue=e,this.messageTypeFormGroup.get("messageType").patchValue(e,{emitEvent:!1}),this.dirty=!0}onFocus(){this.dirty&&(this.messageTypeFormGroup.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0}),this.dirty=!1)}updateView(e){this.modelValue!==e&&(this.modelValue=e,this.propagateChange(this.modelValue))}displayMessageTypeFn(e){return e||void 0}fetchMessageTypes(e,t=!1){return this.searchText=e,Ne(this.messageTypes).pipe(be((n=>n.filter((n=>t?!!e&&n===e:!e||n.toUpperCase().startsWith(e.toUpperCase()))))))}clear(){this.messageTypeFormGroup.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.messageTypeInput.nativeElement.blur(),this.messageTypeInput.nativeElement.focus()}),0)}setDisabledState(e){e?this.messageTypeFormGroup.disable({emitEvent:!1}):this.messageTypeFormGroup.enable({emitEvent:!1})}updateValidators(){this.messageTypeFormGroup.get("messageType").setValidators(this.required?[D.required,D.maxLength(255)]:[D.maxLength(255)]),this.messageTypeFormGroup.get("messageType").updateValueAndValidity({emitEvent:!1})}}e("OutputMessageTypeAutocompleteComponent",ln),ln.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ln,deps:[{token:G.Store},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ln.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:ln,selector:"tb-output-message-type-autocomplete",inputs:{autocompleteHint:"autocompleteHint",subscriptSizing:"subscriptSizing",required:"required"},providers:[{provide:P,useExisting:a((()=>ln)),multi:!0}],viewQueries:[{propertyName:"messageTypeInput",first:!0,predicate:["messageTypeInput"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'\n {{\'tb.rulenode.output-message-type\' | translate}}\n \n \n \n \n \n \n \n {{autocompleteHint | translate}}\n \n {{ \'tb.rulenode.output-message-type-required\' | translate }}\n \n \n {{ \'tb.rulenode.output-message-type-max-length\' | translate }}\n \n\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:ke.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple","hideSingleSelectionIndicator"],exportAs:["matAutocomplete"]},{kind:"directive",type:ke.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"pipe",type:R.AsyncPipe,name:"async"},{kind:"pipe",type:Le.HighlightPipe,name:"highlight"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),Ie([h()],ln.prototype,"required",null),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ln,decorators:[{type:n,args:[{selector:"tb-output-message-type-autocomplete",providers:[{provide:P,useExisting:a((()=>ln)),multi:!0}],template:'\n {{\'tb.rulenode.output-message-type\' | translate}}\n \n \n \n \n \n \n \n {{autocompleteHint | translate}}\n \n {{ \'tb.rulenode.output-message-type-required\' | translate }}\n \n \n {{ \'tb.rulenode.output-message-type-max-length\' | translate }}\n \n\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder}]},propDecorators:{messageTypeInput:[{type:o,args:["messageTypeInput",{static:!0}]}],autocompleteHint:[{type:i}],subscriptSizing:[{type:i}],required:[{type:i}]}});class sn extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.serviceType=p.TB_RULE_ENGINE,this.deduplicationStrategie=gt,this.deduplicationStrategies=Object.keys(this.deduplicationStrategie),this.deduplicationStrategiesTranslations=yt}configForm(){return this.deduplicationConfigForm}onConfigurationSet(e){this.deduplicationConfigForm=this.fb.group({interval:[Y(e?.interval)?e.interval:null,[D.required,D.min(1)]],strategy:[Y(e?.strategy)?e.strategy:null,[D.required]],outMsgType:[Y(e?.outMsgType)?e.outMsgType:null,[D.required]],queueName:[Y(e?.queueName)?e.queueName:null,[D.required]],maxPendingMsgs:[Y(e?.maxPendingMsgs)?e.maxPendingMsgs:null,[D.required,D.min(1),D.max(1e3)]],maxRetries:[Y(e?.maxRetries)?e.maxRetries:null,[D.required,D.min(0),D.max(100)]]})}updateValidators(e){this.deduplicationConfigForm.get("strategy").value===this.deduplicationStrategie.ALL?(this.deduplicationConfigForm.get("outMsgType").enable({emitEvent:!1}),this.deduplicationConfigForm.get("queueName").enable({emitEvent:!1})):(this.deduplicationConfigForm.get("outMsgType").disable({emitEvent:!1}),this.deduplicationConfigForm.get("queueName").disable({emitEvent:!1})),this.deduplicationConfigForm.get("outMsgType").updateValueAndValidity({emitEvent:e}),this.deduplicationConfigForm.get("queueName").updateValueAndValidity({emitEvent:e})}validatorTriggers(){return["strategy"]}}e("DeduplicationConfigComponent",sn),sn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:sn,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),sn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:sn,selector:"tb-action-node-msg-deduplication-config",usesInheritance:!0,ngImport:t,template:"
\n \n {{'tb.rulenode.interval' | translate}}\n \n {{'tb.rulenode.interval-hint' | translate}}\n \n {{'tb.rulenode.interval-required' | translate}}\n \n \n {{'tb.rulenode.interval-min-error' | translate}}\n \n \n \n {{'tb.rulenode.strategy' | translate}}\n \n \n {{ deduplicationStrategiesTranslations.get(strategy) | translate }}\n \n \n \n {{'tb.rulenode.strategy-first-hint' | translate}}\n {{'tb.rulenode.strategy-last-hint' | translate}}\n \n {{'tb.rulenode.strategy-required' | translate}}\n \n \n
\n \n \n \n \n
\n \n \n \n
\n
Advanced settings
\n
\n
\n
\n \n \n {{'tb.rulenode.max-pending-msgs' | translate}}\n \n {{'tb.rulenode.max-pending-msgs-hint' | translate}}\n \n {{'tb.rulenode.max-pending-msgs-required' | translate}}\n \n \n {{'tb.rulenode.max-pending-msgs-max-error' | translate}}\n \n \n {{'tb.rulenode.max-pending-msgs-min-error' | translate}}\n \n \n \n {{'tb.rulenode.max-retries' | translate}}\n \n {{'tb.rulenode.max-retries-hint' | translate}}\n \n {{'tb.rulenode.max-retries-required' | translate}}\n \n \n {{'tb.rulenode.max-retries-max-error' | translate}}\n \n \n {{'tb.rulenode.max-retries-min-error' | translate}}\n \n \n \n
\n
\n",styles:[":host ::ng-deep .mat-expansion-panel.advanced-settings{border:none;box-shadow:none;padding:0}:host ::ng-deep .mat-expansion-panel.advanced-settings .mat-expansion-panel-body{padding:0}:host ::ng-deep .mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:not([aria-disabled=true]):hover{background:white}\n"],dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:Q.QueueAutocompleteComponent,selector:"tb-queue-autocomplete",inputs:["labelText","requiredText","autocompleteHint","subscriptSizing","required","queueType","disabled"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:Te.MatExpansionPanel,selector:"mat-expansion-panel",inputs:["disabled","expanded","hideToggle","togglePosition"],outputs:["opened","closed","expandedChange","afterExpand","afterCollapse"],exportAs:["matExpansionPanel"]},{kind:"component",type:Te.MatExpansionPanelHeader,selector:"mat-expansion-panel-header",inputs:["tabIndex","expandedHeight","collapsedHeight"]},{kind:"directive",type:Te.MatExpansionPanelTitle,selector:"mat-panel-title"},{kind:"directive",type:Te.MatExpansionPanelContent,selector:"ng-template[matExpansionPanelContent]"},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:ln,selector:"tb-output-message-type-autocomplete",inputs:["autocompleteHint","subscriptSizing","required"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:sn,decorators:[{type:n,args:[{selector:"tb-action-node-msg-deduplication-config",template:"
\n \n {{'tb.rulenode.interval' | translate}}\n \n {{'tb.rulenode.interval-hint' | translate}}\n \n {{'tb.rulenode.interval-required' | translate}}\n \n \n {{'tb.rulenode.interval-min-error' | translate}}\n \n \n \n {{'tb.rulenode.strategy' | translate}}\n \n \n {{ deduplicationStrategiesTranslations.get(strategy) | translate }}\n \n \n \n {{'tb.rulenode.strategy-first-hint' | translate}}\n {{'tb.rulenode.strategy-last-hint' | translate}}\n \n {{'tb.rulenode.strategy-required' | translate}}\n \n \n
\n \n \n \n \n
\n \n \n \n
\n
Advanced settings
\n
\n
\n
\n \n \n {{'tb.rulenode.max-pending-msgs' | translate}}\n \n {{'tb.rulenode.max-pending-msgs-hint' | translate}}\n \n {{'tb.rulenode.max-pending-msgs-required' | translate}}\n \n \n {{'tb.rulenode.max-pending-msgs-max-error' | translate}}\n \n \n {{'tb.rulenode.max-pending-msgs-min-error' | translate}}\n \n \n \n {{'tb.rulenode.max-retries' | translate}}\n \n {{'tb.rulenode.max-retries-hint' | translate}}\n \n {{'tb.rulenode.max-retries-required' | translate}}\n \n \n {{'tb.rulenode.max-retries-max-error' | translate}}\n \n \n {{'tb.rulenode.max-retries-min-error' | translate}}\n \n \n \n
\n
\n",styles:[":host ::ng-deep .mat-expansion-panel.advanced-settings{border:none;box-shadow:none;padding:0}:host ::ng-deep .mat-expansion-panel.advanced-settings .mat-expansion-panel-body{padding:0}:host ::ng-deep .mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:not([aria-disabled=true]):hover{background:white}\n"]}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class mn{}e("ExampleHintComponent",mn),mn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:mn,deps:[],target:t.ɵɵFactoryTarget.Component}),mn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:mn,selector:"tb-example-hint",inputs:{hintText:"hintText",popupHelpLink:"popupHelpLink"},ngImport:t,template:'
\n
{{ hintText }}
\n
\n
\n
\n',styles:[":host .custom-styles{display:flex;justify-content:space-between;gap:20px}:host .custom-styles .see-example{display:flex;flex-shrink:0}\n"],dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:Ae.HelpPopupComponent,selector:"[tb-help-popup], [tb-help-popup-content]",inputs:["tb-help-popup","tb-help-popup-content","trigger-text","trigger-style","tb-help-popup-placement","tb-help-popup-style","hintMode"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:mn,decorators:[{type:n,args:[{selector:"tb-example-hint",template:'
\n
{{ hintText }}
\n
\n
\n
\n',styles:[":host .custom-styles{display:flex;justify-content:space-between;gap:20px}:host .custom-styles .see-example{display:flex;flex-shrink:0}\n"]}]}],propDecorators:{hintText:[{type:i}],popupHelpLink:[{type:i}]}});class un extends b{constructor(e,t,n,r){super(e),this.store=e,this.translate=t,this.injector=n,this.fb=r,this.propagateChange=null,this.valueChangeSubscription=null,this.disabled=!1,this.uniqueKeyValuePairValidator=!1,this.required=!1}ngOnInit(){this.ngControl=this.injector.get(V),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))}keyValsFormArray(){return this.kvListFormGroup.get("keyVals")}errorTrigger(){const e=this.keyValsFormArray();for(const t of e.controls)for(const e of Object.keys(t.value))if(t.get(e).touched&&t.get(e).invalid)return!0;return!1}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})}writeValue(e){this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();const t=[];if(e)for(const n of Object.keys(e))Object.prototype.hasOwnProperty.call(e,n)&&t.push(this.fb.group({key:[n,[D.required,D.pattern(/(?:.|\s)*\S(&:.|\s)*/)]],value:[e[n],[D.required,D.pattern(/(?:.|\s)*\S(&:.|\s)*/)]]}));this.kvListFormGroup.setControl("keyVals",this.fb.array(t)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((()=>{this.updateModel()}))}removeKeyVal(e){this.kvListFormGroup.get("keyVals").removeAt(e)}addKeyVal(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[D.required,D.pattern(/(?:.|\s)*\S(&:.|\s)*/)]],value:["",[D.required,D.pattern(/(?:.|\s)*\S(&:.|\s)*/)]]}))}validate(e){const t=this.kvListFormGroup.get("keyVals").value;if(!t.length&&this.required)return{kvMapRequired:!0};if(!this.kvListFormGroup.valid)return{kvFieldsRequired:!0};if(this.uniqueKeyValuePairValidator)for(const e of t)if(e.key===e.value)return{uniqueKeyValuePair:!0};return null}updateModel(){const e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{const t={};e.forEach((e=>{t[e.key]=e.value})),this.propagateChange(t)}}}e("KvMapConfigComponent",un),un.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:un,deps:[{token:G.Store},{token:H.TranslateService},{token:t.Injector},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),un.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:un,selector:"tb-kv-map-config",inputs:{disabled:"disabled",uniqueKeyValuePairValidator:"uniqueKeyValuePairValidator",labelText:"labelText",requiredText:"requiredText",keyText:"keyText",keyRequiredText:"keyRequiredText",valText:"valText",valRequiredText:"valRequiredText",hintText:"hintText",popupHelpLink:"popupHelpLink",required:"required"},providers:[{provide:P,useExisting:a((()=>un)),multi:!0},{provide:w,useExisting:a((()=>un)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n
\n
{{ labelText }}
\n
\n tb.rulenode.map-fields-required\n
\n
\n
\n
\n
\n
{{ keyText }}
\n
{{ valText }}
\n
\n
\n
\n
\n \n \n \n \n \n \n
\n \n
\n
\n
\n
\n
\n
\n \n
\n \n
\n',styles:[":host .field-space{flex:1 1 50%}:host .actions-header{width:40px}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"directive",type:R.NgClass,selector:"[ngClass]",inputs:["class","ngClass"]},{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"component",type:te.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:fe.DefaultClassDirective,selector:" [ngClass], [ngClass.xs], [ngClass.sm], [ngClass.md], [ngClass.lg], [ngClass.xl], [ngClass.lt-sm], [ngClass.lt-md], [ngClass.lt-lg], [ngClass.lt-xl], [ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg]",inputs:["ngClass","ngClass.xs","ngClass.sm","ngClass.md","ngClass.lg","ngClass.xl","ngClass.lt-sm","ngClass.lt-md","ngClass.lt-lg","ngClass.lt-xl","ngClass.gt-xs","ngClass.gt-sm","ngClass.gt-md","ngClass.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormControlDirective,selector:"[formControl]",inputs:["formControl","disabled","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:mn,selector:"tb-example-hint",inputs:["hintText","popupHelpLink"]},{kind:"pipe",type:R.AsyncPipe,name:"async"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),Ie([h()],un.prototype,"disabled",void 0),Ie([h()],un.prototype,"uniqueKeyValuePairValidator",void 0),Ie([h()],un.prototype,"required",void 0),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:un,decorators:[{type:n,args:[{selector:"tb-kv-map-config",providers:[{provide:P,useExisting:a((()=>un)),multi:!0},{provide:w,useExisting:a((()=>un)),multi:!0}],template:'
\n
\n
{{ labelText }}
\n
\n tb.rulenode.map-fields-required\n
\n
\n
\n
\n
\n
{{ keyText }}
\n
{{ valText }}
\n
\n
\n
\n
\n \n \n \n \n \n \n
\n \n
\n
\n
\n
\n
\n
\n \n
\n \n
\n',styles:[":host .field-space{flex:1 1 50%}:host .actions-header{width:40px}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:H.TranslateService},{type:t.Injector},{type:E.FormBuilder}]},propDecorators:{disabled:[{type:i}],uniqueKeyValuePairValidator:[{type:i}],labelText:[{type:i}],requiredText:[{type:i}],keyText:[{type:i}],keyRequiredText:[{type:i}],valText:[{type:i}],valRequiredText:[{type:i}],hintText:[{type:i}],popupHelpLink:[{type:i}],required:[{type:i}]}});class pn extends b{get required(){return this.requiredValue}set required(e){this.requiredValue=de(e)}constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.values(g),this.directionTypeTranslations=y,this.entityType=x,this.propagateChange=null}ngOnInit(){this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[D.required]],maxLevel:[null,[D.min(1)]],relationType:[null],deviceTypes:[null,[D.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((e=>{this.deviceRelationsQueryFormGroup.valid?this.propagateChange(e):this.propagateChange(null)}))}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})}writeValue(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})}}e("DeviceRelationsQueryConfigComponent",pn),pn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:pn,deps:[{token:G.Store},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),pn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:pn,selector:"tb-device-relations-query-config",inputs:{disabled:"disabled",required:"required"},providers:[{provide:P,useExisting:a((()=>pn)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n {{ \'tb.rulenode.max-relation-level-error\' | translate }}\n \n \n
\n
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n \n \n help\n \n
\n',styles:[":host .last-level-slide-toggle{margin:8px 0 24px}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:Me.EntitySubTypeListComponent,selector:"tb-entity-subtype-list",inputs:["required","floatLabel","label","disabled","entityType","emptyInputPlaceholder","filledInputPlaceholder"]},{kind:"component",type:Ge.RelationTypeAutocompleteComponent,selector:"tb-relation-type-autocomplete",inputs:["showLabel","additionalClasses","appearance","required","disabled","subscriptSizing"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"component",type:Ee.MatSlideToggle,selector:"mat-slide-toggle",inputs:["disabled","disableRipple","color","tabIndex"],exportAs:["matSlideToggle"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:De.HintTooltipIconComponent,selector:"[tb-hint-tooltip-icon]",inputs:["tb-hint-tooltip-icon","tooltipPosition","hintIcon"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:pn,decorators:[{type:n,args:[{selector:"tb-device-relations-query-config",providers:[{provide:P,useExisting:a((()=>pn)),multi:!0}],template:'
\n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n {{ \'tb.rulenode.max-relation-level-error\' | translate }}\n \n \n
\n
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n \n \n help\n \n
\n',styles:[":host .last-level-slide-toggle{margin:8px 0 24px}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder}]},propDecorators:{disabled:[{type:i}],required:[{type:i}]}});class dn extends b{get required(){return this.requiredValue}set required(e){this.requiredValue=de(e)}constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.values(g),this.directionTypeTranslations=y,this.propagateChange=null}ngOnInit(){this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[D.required]],maxLevel:[null,[D.min(1)]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((e=>{this.relationsQueryFormGroup.valid?this.propagateChange(e):this.propagateChange(null)}))}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})}writeValue(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})}}e("RelationsQueryConfigComponent",dn),dn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:dn,deps:[{token:G.Store},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),dn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:dn,selector:"tb-relations-query-config",inputs:{disabled:"disabled",required:"required"},providers:[{provide:P,useExisting:a((()=>dn)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n
tb.rulenode.relations-query
\n
\n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n {{ \'tb.rulenode.max-relation-level-error\' | translate }}\n \n \n
\n
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n
\n
\n
relation.relation-filters
\n \n \n
\n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:Ee.MatSlideToggle,selector:"mat-slide-toggle",inputs:["disabled","disableRipple","color","tabIndex"],exportAs:["matSlideToggle"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:De.HintTooltipIconComponent,selector:"[tb-hint-tooltip-icon]",inputs:["tb-hint-tooltip-icon","tooltipPosition","hintIcon"]},{kind:"component",type:Ve.RelationFiltersComponent,selector:"tb-relation-filters",inputs:["disabled","allowedEntityTypes"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:dn,decorators:[{type:n,args:[{selector:"tb-relations-query-config",providers:[{provide:P,useExisting:a((()=>dn)),multi:!0}],template:'
\n
tb.rulenode.relations-query
\n
\n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n {{ \'tb.rulenode.max-relation-level-error\' | translate }}\n \n \n
\n
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n
\n
\n
relation.relation-filters
\n \n \n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder}]},propDecorators:{disabled:[{type:i}],required:[{type:i}]}});class cn extends b{get required(){return this.requiredValue}set required(e){this.requiredValue=de(e)}constructor(e,t,n,r){super(e),this.store=e,this.translate=t,this.truncate=n,this.fb=r,this.placeholder="tb.rulenode.add-message-type",this.separatorKeysCodes=[ae,ie,le],this.messageTypes=[],this.messageTypesList=[],this.searchText="",this.propagateChange=e=>{},this.messageTypeConfigForm=this.fb.group({messageType:[null]});for(const e of Object.keys(v))this.messageTypesList.push({name:C.get(v[e]),value:e})}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}ngOnInit(){this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(ve(""),be((e=>e||"")),he((e=>this.fetchMessageTypes(e))),Ce())}setDisabledState(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})}writeValue(e){this.searchText="",this.messageTypes.length=0,e&&e.forEach((e=>{const t=this.messageTypesList.find((t=>t.value===e));t?this.messageTypes.push({name:t.name,value:t.value}):this.messageTypes.push({name:e,value:e})}))}displayMessageTypeFn(e){return e?e.name:void 0}textIsNotEmpty(e){return e&&e.length>0}createMessageType(e,t){e.preventDefault(),this.transformMessageType(t)}add(e){this.transformMessageType(e.value)}fetchMessageTypes(e){if(this.searchText=e,this.searchText&&this.searchText.length){const e=this.searchText.toUpperCase();return Ne(this.messageTypesList.filter((t=>t.name.toUpperCase().includes(e))))}return Ne(this.messageTypesList)}transformMessageType(e){if((e||"").trim()){let t;const n=e.trim(),r=this.messageTypesList.find((e=>e.name===n));t=r?{name:r.name,value:r.value}:{name:n,value:n},t&&this.addMessageType(t)}this.clear("")}remove(e){const t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())}selected(e){this.addMessageType(e.option.value),this.clear("")}addMessageType(e){-1===this.messageTypes.findIndex((t=>t.value===e.value))&&(this.messageTypes.push(e),this.updateModel())}onFocus(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})}clear(e=""){this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.messageTypeInput.nativeElement.blur(),this.messageTypeInput.nativeElement.focus()}),0)}updateModel(){const e=this.messageTypes.map((e=>e.value));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))}}e("MessageTypesConfigComponent",cn),cn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:cn,deps:[{token:G.Store},{token:H.TranslateService},{token:F.TruncatePipe},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),cn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:cn,selector:"tb-message-types-config",inputs:{required:"required",label:"label",placeholder:"placeholder",disabled:"disabled"},providers:[{provide:P,useExisting:a((()=>cn)),multi:!0}],viewQueries:[{propertyName:"chipList",first:!0,predicate:["chipList"],descendants:!0},{propertyName:"matAutocomplete",first:!0,predicate:["messageTypeAutocomplete"],descendants:!0},{propertyName:"messageTypeInput",first:!0,predicate:["messageTypeInput"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ \'tb.rulenode.no-message-type-matching\' | translate :\n {messageType: truncate.transform(searchText, true, 6, '...')}\n }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n help\n \n {{ \'tb.rulenode.select-message-types-required\' | translate }}\n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"component",type:ke.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple","hideSingleSelectionIndicator"],exportAs:["matAutocomplete"]},{kind:"directive",type:ke.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{kind:"directive",type:ke.MatAutocompleteOrigin,selector:"[matAutocompleteOrigin]",exportAs:["matAutocompleteOrigin"]},{kind:"component",type:se.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:se.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:se.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:se.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:R.AsyncPipe,name:"async"},{kind:"pipe",type:Le.HighlightPipe,name:"highlight"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:cn,decorators:[{type:n,args:[{selector:"tb-message-types-config",providers:[{provide:P,useExisting:a((()=>cn)),multi:!0}],template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ \'tb.rulenode.no-message-type-matching\' | translate :\n {messageType: truncate.transform(searchText, true, 6, '...')}\n }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n help\n \n {{ \'tb.rulenode.select-message-types-required\' | translate }}\n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:H.TranslateService},{type:F.TruncatePipe},{type:E.FormBuilder}]},propDecorators:{required:[{type:i}],label:[{type:i}],placeholder:[{type:i}],disabled:[{type:i}],chipList:[{type:o,args:["chipList",{static:!1}]}],matAutocomplete:[{type:o,args:["messageTypeAutocomplete",{static:!1}]}],messageTypeInput:[{type:o,args:["messageTypeInput",{static:!1}]}]}});class fn extends b{get required(){return this.requiredValue}set required(e){this.requiredValue=de(e)}constructor(e,t){super(e),this.store=e,this.fb=t,this.subscriptions=[],this.disableCertPemCredentials=!1,this.passwordFieldRequired=!0,this.allCredentialsTypes=Tt,this.credentialsTypeTranslationsMap=It,this.propagateChange=e=>{}}ngOnInit(){this.credentialsConfigFormGroup=this.fb.group({type:[null,[D.required]],username:[null,[]],password:[null,[]],caCert:[null,[]],caCertFileName:[null,[]],privateKey:[null,[]],privateKeyFileName:[null,[]],cert:[null,[]],certFileName:[null,[]]}),this.subscriptions.push(this.credentialsConfigFormGroup.valueChanges.subscribe((()=>{this.updateView()}))),this.subscriptions.push(this.credentialsConfigFormGroup.get("type").valueChanges.subscribe((()=>{this.credentialsTypeChanged()})))}ngOnChanges(e){for(const t of Object.keys(e)){const n=e[t];if(!n.firstChange&&n.currentValue!==n.previousValue&&n.currentValue&&"disableCertPemCredentials"===t){"cert.PEM"===this.credentialsConfigFormGroup.get("type").value&&setTimeout((()=>{this.credentialsConfigFormGroup.get("type").patchValue("anonymous",{emitEvent:!0})}))}}}ngOnDestroy(){this.subscriptions.forEach((e=>e.unsubscribe()))}writeValue(e){Y(e)&&(this.credentialsConfigFormGroup.reset(e,{emitEvent:!1}),this.updateValidators())}setDisabledState(e){e?this.credentialsConfigFormGroup.disable({emitEvent:!1}):(this.credentialsConfigFormGroup.enable({emitEvent:!1}),this.updateValidators())}updateView(){let e=this.credentialsConfigFormGroup.value;const t=e.type;switch(t){case"anonymous":e={type:t};break;case"basic":e={type:t,username:e.username,password:e.password};break;case"cert.PEM":delete e.username}this.propagateChange(e)}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}validate(e){return this.credentialsConfigFormGroup.valid?null:{credentialsConfig:{valid:!1}}}credentialsTypeChanged(){this.credentialsConfigFormGroup.patchValue({username:null,password:null,caCert:null,caCertFileName:null,privateKey:null,privateKeyFileName:null,cert:null,certFileName:null}),this.updateValidators()}updateValidators(e=!1){const t=this.credentialsConfigFormGroup.get("type").value;switch(e&&this.credentialsConfigFormGroup.reset({type:t},{emitEvent:!1}),this.credentialsConfigFormGroup.setValidators([]),this.credentialsConfigFormGroup.get("username").setValidators([]),this.credentialsConfigFormGroup.get("password").setValidators([]),t){case"anonymous":break;case"basic":this.credentialsConfigFormGroup.get("username").setValidators([D.required]),this.credentialsConfigFormGroup.get("password").setValidators(this.passwordFieldRequired?[D.required]:[]);break;case"cert.PEM":this.credentialsConfigFormGroup.setValidators([this.requiredFilesSelected(D.required,[["caCert","caCertFileName"],["privateKey","privateKeyFileName","cert","certFileName"]])])}this.credentialsConfigFormGroup.get("username").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.get("password").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.updateValueAndValidity({emitEvent:e})}requiredFilesSelected(e,t=null){return n=>{t||(t=[Object.keys(n.controls)]);return n?.controls&&t.some((t=>t.every((t=>!e(n.controls[t])))))?null:{notAllRequiredFilesSelected:!0}}}}e("CredentialsConfigComponent",fn),fn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:fn,deps:[{token:G.Store},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),fn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:fn,selector:"tb-credentials-config",inputs:{required:"required",disableCertPemCredentials:"disableCertPemCredentials",passwordFieldRequired:"passwordFieldRequired"},providers:[{provide:P,useExisting:a((()=>fn)),multi:!0},{provide:w,useExisting:a((()=>fn)),multi:!0}],usesInheritance:!0,usesOnChanges:!0,ngImport:t,template:'
\n \n \n tb.rulenode.credentials\n \n {{ credentialsTypeTranslationsMap.get(credentialsConfigFormGroup.get(\'type\').value) | translate }}\n \n \n \n \n tb.rulenode.credentials-type\n \n \n {{ credentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n
{{ \'tb.rulenode.credentials-pem-hint\' | translate }}
\n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:R.NgSwitch,selector:"[ngSwitch]",inputs:["ngSwitch"]},{kind:"directive",type:R.NgSwitchCase,selector:"[ngSwitchCase]",inputs:["ngSwitchCase"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:Te.MatExpansionPanel,selector:"mat-expansion-panel",inputs:["disabled","expanded","hideToggle","togglePosition"],outputs:["opened","closed","expandedChange","afterExpand","afterCollapse"],exportAs:["matExpansionPanel"]},{kind:"component",type:Te.MatExpansionPanelHeader,selector:"mat-expansion-panel-header",inputs:["tabIndex","expandedHeight","collapsedHeight"]},{kind:"directive",type:Te.MatExpansionPanelTitle,selector:"mat-panel-title"},{kind:"directive",type:Te.MatExpansionPanelDescription,selector:"mat-panel-description"},{kind:"directive",type:Te.MatExpansionPanelContent,selector:"ng-template[matExpansionPanelContent]"},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:Pe.FileInputComponent,selector:"tb-file-input",inputs:["label","accept","noFileText","inputId","allowedExtensions","dropLabel","contentConvertFunction","required","requiredAsError","disabled","existingFileName","readAsBinary","workFromFileObj","multipleFile"],outputs:["fileNameChanged"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:we.TogglePasswordComponent,selector:"tb-toggle-password"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:fn,decorators:[{type:n,args:[{selector:"tb-credentials-config",providers:[{provide:P,useExisting:a((()=>fn)),multi:!0},{provide:w,useExisting:a((()=>fn)),multi:!0}],template:'
\n \n \n tb.rulenode.credentials\n \n {{ credentialsTypeTranslationsMap.get(credentialsConfigFormGroup.get(\'type\').value) | translate }}\n \n \n \n \n tb.rulenode.credentials-type\n \n \n {{ credentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n
{{ \'tb.rulenode.credentials-pem-hint\' | translate }}
\n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder}]},propDecorators:{required:[{type:i}],disableCertPemCredentials:[{type:i}],passwordFieldRequired:[{type:i}]}});class gn{constructor(e,t){this.fb=e,this.translate=t,this.propagateChange=e=>{},this.destroy$=new Se,this.selectOptions=[];for(const e of wt.keys())this.selectOptions.push({value:e,name:this.translate.instant(wt.get(e))})}ngOnInit(){this.chipControlGroup=this.fb.group({chipControl:[null,[]]}),this.chipControlGroup.get("chipControl").valueChanges.pipe(Fe(this.destroy$)).subscribe((e=>{e&&this.propagateChange(e)}))}writeValue(e){this.chipControlGroup.get("chipControl").patchValue(e,{emitEvent:!1})}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){e?this.chipControlGroup.disable({emitEvent:!1}):this.chipControlGroup.enable({emitEvent:!1})}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}}e("MsgMetadataChipComponent",gn),gn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:gn,deps:[{token:E.FormBuilder},{token:H.TranslateService}],target:t.ɵɵFactoryTarget.Component}),gn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:gn,selector:"tb-msg-metadata-chip",inputs:{labelText:"labelText"},providers:[{provide:P,useExisting:a((()=>gn)),multi:!0}],ngImport:t,template:'
\n
{{ labelText }}
\n \n {{ option.name }}\n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"component",type:se.MatChipListbox,selector:"mat-chip-listbox",inputs:["tabIndex","multiple","aria-orientation","selectable","compareWith","required","hideSingleSelectionIndicator","value"],outputs:["change"]},{kind:"component",type:se.MatChipOption,selector:"mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]",inputs:["color","disabled","disableRipple","tabIndex","selectable","selected"],outputs:["selectionChange"]},{kind:"directive",type:fe.DefaultClassDirective,selector:" [ngClass], [ngClass.xs], [ngClass.sm], [ngClass.md], [ngClass.lg], [ngClass.xl], [ngClass.lt-sm], [ngClass.lt-md], [ngClass.lt-lg], [ngClass.lt-xl], [ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg]",inputs:["ngClass","ngClass.xs","ngClass.sm","ngClass.md","ngClass.lg","ngClass.xl","ngClass.lt-sm","ngClass.lt-md","ngClass.lt-lg","ngClass.lt-xl","ngClass.gt-xs","ngClass.gt-sm","ngClass.gt-md","ngClass.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:gn,decorators:[{type:n,args:[{selector:"tb-msg-metadata-chip",providers:[{provide:P,useExisting:a((()=>gn)),multi:!0}],template:'
\n
{{ labelText }}
\n \n {{ option.name }}\n \n
\n'}]}],ctorParameters:function(){return[{type:E.FormBuilder},{type:H.TranslateService}]},propDecorators:{labelText:[{type:i}]}});class yn extends b{constructor(e,t,n,r){super(e),this.store=e,this.translate=t,this.injector=n,this.fb=r,this.destroy$=new Se,this.sourceFieldSubcritption=[],this.propagateChange=null,this.valueChangeSubscription=null,this.disabled=!1,this.required=!1}ngOnInit(){this.ngControl=this.injector.get(V),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.svListFormGroup=this.fb.group({}),this.svListFormGroup.addControl("keyVals",this.fb.array([]))}keyValsFormArray(){return this.svListFormGroup.get("keyVals")}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.svListFormGroup.disable({emitEvent:!1}):this.svListFormGroup.enable({emitEvent:!1})}writeValue(e){this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();const t=[];if(e)for(const n of Object.keys(e))Object.prototype.hasOwnProperty.call(e,n)&&t.push(this.fb.group({key:[n,[D.required]],value:[e[n],[D.required,D.pattern(/(?:.|\s)*\S(&:.|\s)*/)]]}));this.svListFormGroup.setControl("keyVals",this.fb.array(t));for(const e of this.keyValsFormArray().controls)this.keyChangeSubscribe(e);this.valueChangeSubscription=this.svListFormGroup.valueChanges.pipe(Fe(this.destroy$)).subscribe((()=>{this.updateModel()}))}errorTrigger(){const e=this.keyValsFormArray();for(const t of e.controls)for(const e of Object.keys(t.value))if(t.get(e).touched&&t.get(e).invalid)return!0;return!1}filterSelectOptions(e){const t=[];for(const e of this.svListFormGroup.get("keyVals").value){const n=this.selectOptions.find((t=>t.value===e.key));n&&t.push(n)}const n=[];for(const r of this.selectOptions)Y(t.find((e=>e.value===r.value)))&&r.value!==e?.get("key").value||n.push(r);return n}removeKeyVal(e){this.svListFormGroup.get("keyVals").removeAt(e),this.sourceFieldSubcritption[e].unsubscribe(),this.sourceFieldSubcritption.splice(e,1)}addKeyVal(){const e=this.svListFormGroup.get("keyVals");e.push(this.fb.group({key:["",[D.required]],value:["",[D.required,D.pattern(/(?:.|\s)*\S(&:.|\s)*/)]]})),this.keyChangeSubscribe(e.at(e.length-1))}keyChangeSubscribe(e){this.sourceFieldSubcritption.push(e.get("key").valueChanges.pipe(Fe(this.destroy$)).subscribe((t=>{e.get("value").patchValue(this.targetKeyPrefix+t[0].toUpperCase()+t.slice(1))})))}validate(e){return!this.svListFormGroup.get("keyVals").value.length&&this.required?{svMapRequired:!0}:this.svListFormGroup.valid?null:{svFieldsRequired:!0}}updateModel(){const e=this.svListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.svListFormGroup.valid)this.propagateChange(null);else{const t={};e.forEach((e=>{t[e.key]=e.value})),this.propagateChange(t)}}}e("SvMapConfigComponent",yn),yn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:yn,deps:[{token:G.Store},{token:H.TranslateService},{token:t.Injector},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),yn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:yn,selector:"tb-sv-map-config",inputs:{selectOptions:"selectOptions",disabled:"disabled",labelText:"labelText",requiredText:"requiredText",targetKeyPrefix:"targetKeyPrefix",selectText:"selectText",selectRequiredText:"selectRequiredText",valText:"valText",valRequiredText:"valRequiredText",hintText:"hintText",popupHelpLink:"popupHelpLink",required:"required"},providers:[{provide:P,useExisting:a((()=>yn)),multi:!0},{provide:w,useExisting:a((()=>yn)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n
\n
tb.rulenode.originator-fields-mapping
\n
\n tb.rulenode.map-fields-required\n
\n
\n
\n
\n
\n
{{ selectText }}
\n
{{ valText }}
\n
\n
\n
\n
\n \n \n \n {{option.name}}\n \n \n \n \n \n \n
\n \n
\n
\n
\n
\n
\n
\n \n
\n \n
\n',styles:[":host .field-space{flex:1 1 50%}:host .actions-header{width:40px}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"directive",type:R.NgClass,selector:"[ngClass]",inputs:["class","ngClass"]},{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"component",type:te.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:fe.DefaultClassDirective,selector:" [ngClass], [ngClass.xs], [ngClass.sm], [ngClass.md], [ngClass.lg], [ngClass.xl], [ngClass.lt-sm], [ngClass.lt-md], [ngClass.lt-lg], [ngClass.lt-xl], [ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg]",inputs:["ngClass","ngClass.xs","ngClass.sm","ngClass.md","ngClass.lg","ngClass.xl","ngClass.lt-sm","ngClass.lt-md","ngClass.lt-lg","ngClass.lt-xl","ngClass.gt-xs","ngClass.gt-sm","ngClass.gt-md","ngClass.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormControlDirective,selector:"[formControl]",inputs:["formControl","disabled","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:mn,selector:"tb-example-hint",inputs:["hintText","popupHelpLink"]},{kind:"pipe",type:R.AsyncPipe,name:"async"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),Ie([h()],yn.prototype,"disabled",void 0),Ie([h()],yn.prototype,"required",void 0),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:yn,decorators:[{type:n,args:[{selector:"tb-sv-map-config",providers:[{provide:P,useExisting:a((()=>yn)),multi:!0},{provide:w,useExisting:a((()=>yn)),multi:!0}],template:'
\n
\n
tb.rulenode.originator-fields-mapping
\n
\n tb.rulenode.map-fields-required\n
\n
\n
\n
\n
\n
{{ selectText }}
\n
{{ valText }}
\n
\n
\n
\n
\n \n \n \n {{option.name}}\n \n \n \n \n \n \n
\n \n
\n
\n
\n
\n
\n
\n \n
\n \n
\n',styles:[":host .field-space{flex:1 1 50%}:host .actions-header{width:40px}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:H.TranslateService},{type:t.Injector},{type:E.FormBuilder}]},propDecorators:{selectOptions:[{type:i}],disabled:[{type:i}],labelText:[{type:i}],requiredText:[{type:i}],targetKeyPrefix:[{type:i}],selectText:[{type:i}],selectRequiredText:[{type:i}],valText:[{type:i}],valRequiredText:[{type:i}],hintText:[{type:i}],popupHelpLink:[{type:i}],required:[{type:i}]}});class xn extends b{get required(){return this.requiredValue}set required(e){this.requiredValue=de(e)}constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(g),this.directionTypeTranslations=y,this.propagateChange=null}ngOnInit(){this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[D.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((e=>{this.relationsQueryFormGroup.valid?this.propagateChange(e):this.propagateChange(null)}))}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})}writeValue(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})}}e("RelationsQueryConfigOldComponent",xn),xn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:xn,deps:[{token:G.Store},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),xn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:xn,selector:"tb-relations-query-config-old",inputs:{disabled:"disabled",required:"required"},providers:[{provide:P,useExisting:a((()=>xn)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Ve.RelationFiltersComponent,selector:"tb-relation-filters",inputs:["disabled","allowedEntityTypes"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:xn,decorators:[{type:n,args:[{selector:"tb-relations-query-config-old",providers:[{provide:P,useExisting:a((()=>xn)),multi:!0}],template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder}]},propDecorators:{disabled:[{type:i}],required:[{type:i}]}});class bn{constructor(e,t){this.translate=e,this.fb=t,this.propagateChange=e=>{},this.destroy$=new Se,this.separatorKeysCodes=[ae,ie,le]}ngOnInit(){this.attributeControlGroup=this.fb.group({clientAttributeNames:[null,[]],sharedAttributeNames:[null,[]],serverAttributeNames:[null,[]],latestTsKeyNames:[null,[]],getLatestValueWithTs:[!1,[]]},{validators:this.atLeastOne(D.required,["clientAttributeNames","sharedAttributeNames","serverAttributeNames","latestTsKeyNames"])}),this.attributeControlGroup.valueChanges.pipe(Fe(this.destroy$)).subscribe((e=>{this.propagateChange(e)}))}validate(){return this.attributeControlGroup.valid?null:{atLeastOneRequired:!0}}atLeastOne(e,t=null){return n=>{t||(t=Object.keys(n.controls));return n?.controls&&t.some((t=>!e(n.controls[t])))?null:{atLeastOne:!0}}}writeValue(e){this.attributeControlGroup.setValue(e,{emitEvent:!1})}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){e?this.attributeControlGroup.disable({emitEvent:!1}):this.attributeControlGroup.enable({emitEvent:!1})}ngOnDestroy(){this.destroy$.next(null),this.destroy$.complete()}}e("SelectAttributesComponent",bn),bn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:bn,deps:[{token:H.TranslateService},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),bn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:bn,selector:"tb-select-attributes",inputs:{popupHelpLink:"popupHelpLink"},providers:[{provide:P,useExisting:a((()=>bn)),multi:!0},{provide:w,useExisting:bn,multi:!0}],ngImport:t,template:'
\n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n {{ \'tb.rulenode.fetch-latest-telemetry-with-timestamp\' | translate }}\n \n
\n
\n\n\n help\n\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:R.NgTemplateOutlet,selector:"[ngTemplateOutlet]",inputs:["ngTemplateOutletContext","ngTemplateOutlet","ngTemplateOutletInjector"]},{kind:"component",type:Re.StringItemsListComponent,selector:"tb-string-items-list",inputs:["required","disabled","label","placeholder","hint","requiredText","floatLabel","appearance","editable","subscriptSizing","predefinedValues"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"component",type:Ee.MatSlideToggle,selector:"mat-slide-toggle",inputs:["disabled","disableRipple","color","tabIndex"],exportAs:["matSlideToggle"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:De.HintTooltipIconComponent,selector:"[tb-hint-tooltip-icon]",inputs:["tb-hint-tooltip-icon","tooltipPosition","hintIcon"]},{kind:"component",type:mn,selector:"tb-example-hint",inputs:["hintText","popupHelpLink"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:bn,decorators:[{type:n,args:[{selector:"tb-select-attributes",providers:[{provide:P,useExisting:a((()=>bn)),multi:!0},{provide:w,useExisting:bn,multi:!0}],template:'
\n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n {{ \'tb.rulenode.fetch-latest-telemetry-with-timestamp\' | translate }}\n \n
\n
\n\n\n help\n\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:H.TranslateService},{type:E.FormBuilder}]},propDecorators:{popupHelpLink:[{type:i}]}});class hn extends b{constructor(e,t){super(e),this.store=e,this.fb=t,this.propagateChange=null,this.destroy$=new Se,this.alarmStatus=k,this.alarmStatusTranslations=L}ngOnInit(){this.alarmStatusGroup=this.fb.group({alarmStatus:[null,[]]}),this.alarmStatusGroup.get("alarmStatus").valueChanges.pipe(Fe(this.destroy$)).subscribe((e=>{this.propagateChange(e)}))}setDisabledState(e){e?this.alarmStatusGroup.disable({emitEvent:!1}):this.alarmStatusGroup.enable({emitEvent:!1})}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}writeValue(e){this.alarmStatusGroup.get("alarmStatus").patchValue(e,{emitEvent:!1})}}e("AlarmStatusSelectComponent",hn),hn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:hn,deps:[{token:G.Store},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),hn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:hn,selector:"tb-alarm-status-select",providers:[{provide:P,useExisting:a((()=>hn)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n
\n \n {{ alarmStatusTranslations.get(alarmStatus.ACTIVE_UNACK) | translate }}\n \n \n {{ alarmStatusTranslations.get(alarmStatus.ACTIVE_ACK) | translate }}\n \n
\n
\n \n {{ alarmStatusTranslations.get(alarmStatus.CLEARED_UNACK) | translate }}\n \n \n {{ alarmStatusTranslations.get(alarmStatus.ACTIVE_UNACK) | translate }}\n \n
\n
\n
\n',styles:[":host .chip-listbox{max-width:460px;width:100%}:host .chip-listbox .toggle-column{display:flex;flex:1 1 100%;gap:8px}:host .chip-listbox .option{margin:0}@media screen and (max-width: 959px){:host .chip-listbox{max-width:360px}:host .chip-listbox .toggle-column{flex-direction:column}}:host ::ng-deep .chip-listbox .mdc-evolution-chip-set__chips{gap:8px}:host ::ng-deep .chip-listbox .option button{flex-basis:100%;justify-content:start}:host ::ng-deep .chip-listbox .option .mdc-evolution-chip__graphic{flex-grow:0}\n"],dependencies:[{kind:"component",type:se.MatChipListbox,selector:"mat-chip-listbox",inputs:["tabIndex","multiple","aria-orientation","selectable","compareWith","required","hideSingleSelectionIndicator","value"],outputs:["change"]},{kind:"component",type:se.MatChipOption,selector:"mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]",inputs:["color","disabled","disableRipple","tabIndex","selectable","selected"],outputs:["selectionChange"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:hn,decorators:[{type:n,args:[{selector:"tb-alarm-status-select",providers:[{provide:P,useExisting:a((()=>hn)),multi:!0}],template:'
\n \n
\n \n {{ alarmStatusTranslations.get(alarmStatus.ACTIVE_UNACK) | translate }}\n \n \n {{ alarmStatusTranslations.get(alarmStatus.ACTIVE_ACK) | translate }}\n \n
\n
\n \n {{ alarmStatusTranslations.get(alarmStatus.CLEARED_UNACK) | translate }}\n \n \n {{ alarmStatusTranslations.get(alarmStatus.ACTIVE_UNACK) | translate }}\n \n
\n
\n
\n',styles:[":host .chip-listbox{max-width:460px;width:100%}:host .chip-listbox .toggle-column{display:flex;flex:1 1 100%;gap:8px}:host .chip-listbox .option{margin:0}@media screen and (max-width: 959px){:host .chip-listbox{max-width:360px}:host .chip-listbox .toggle-column{flex-direction:column}}:host ::ng-deep .chip-listbox .mdc-evolution-chip-set__chips{gap:8px}:host ::ng-deep .chip-listbox .option button{flex-basis:100%;justify-content:start}:host ::ng-deep .chip-listbox .option .mdc-evolution-chip__graphic{flex-grow:0}\n"]}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder}]}});class vn{}e("RulenodeCoreConfigCommonModule",vn),vn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:vn,deps:[],target:t.ɵɵFactoryTarget.NgModule}),vn.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:vn,declarations:[un,pn,dn,cn,fn,rn,on,ln,Xt,gn,yn,xn,bn,hn,mn],imports:[O,T,qe],exports:[un,pn,dn,cn,fn,rn,on,ln,Xt,gn,yn,xn,bn,hn,mn]}),vn.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:vn,imports:[O,T,qe]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:vn,decorators:[{type:l,args:[{declarations:[un,pn,dn,cn,fn,rn,on,ln,Xt,gn,yn,xn,bn,hn,mn],imports:[O,T,qe],exports:[un,pn,dn,cn,fn,rn,on,ln,Xt,gn,yn,xn,bn,hn,mn]}]}]});class Cn{}e("RuleNodeCoreConfigActionModule",Cn),Cn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Cn,deps:[],target:t.ɵɵFactoryTarget.NgModule}),Cn.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:Cn,declarations:[nn,We,en,Wt,zt,Ye,Ze,et,tt,Qt,nt,ot,Ht,jt,Yt,Zt,tn,Xe,rt,Jt,$t,an,sn],imports:[O,T,qe,vn],exports:[nn,We,en,Wt,zt,Ye,Ze,et,tt,Qt,nt,ot,Ht,jt,Yt,Zt,tn,Xe,rt,Jt,$t,an,sn]}),Cn.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Cn,imports:[O,T,qe,vn]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Cn,decorators:[{type:l,args:[{declarations:[nn,We,en,Wt,zt,Ye,Ze,et,tt,Qt,nt,ot,Ht,jt,Yt,Zt,tn,Xe,rt,Jt,$t,an,sn],imports:[O,T,qe,vn],exports:[nn,We,en,Wt,zt,Ye,Ze,et,tt,Qt,nt,ot,Ht,jt,Yt,Zt,tn,Xe,rt,Jt,$t,an,sn]}]}]});class Fn extends s{constructor(e,t,n){super(e),this.store=e,this.translate=t,this.fb=n,this.separatorKeysCodes=[ae,ie,le]}configForm(){return this.calculateDeltaConfigForm}onConfigurationSet(e){this.calculateDeltaConfigForm=this.fb.group({inputValueKey:[e.inputValueKey,[D.required,D.pattern(/(?:.|\s)*\S(&:.|\s)*/)]],outputValueKey:[e.outputValueKey,[D.required,D.pattern(/(?:.|\s)*\S(&:.|\s)*/)]],useCache:[e.useCache,[]],addPeriodBetweenMsgs:[e.addPeriodBetweenMsgs,[]],periodValueKey:[e.periodValueKey,[]],round:[e.round,[D.min(0),D.max(15)]],tellFailureIfDeltaIsNegative:[e.tellFailureIfDeltaIsNegative,[]]})}prepareInputConfig(e){return{inputValueKey:Y(e?.inputValueKey)?e.inputValueKey:null,outputValueKey:Y(e?.outputValueKey)?e.outputValueKey:null,useCache:!Y(e?.useCache)||e.useCache,addPeriodBetweenMsgs:!!Y(e?.addPeriodBetweenMsgs)&&e.addPeriodBetweenMsgs,periodValueKey:Y(e?.periodValueKey)?e.periodValueKey:null,round:Y(e?.round)?e.round:null,tellFailureIfDeltaIsNegative:!Y(e?.tellFailureIfDeltaIsNegative)||e.tellFailureIfDeltaIsNegative}}prepareOutputConfig(e){return W(e)}updateValidators(e){this.calculateDeltaConfigForm.get("addPeriodBetweenMsgs").value?this.calculateDeltaConfigForm.get("periodValueKey").setValidators([D.required]):this.calculateDeltaConfigForm.get("periodValueKey").setValidators([]),this.calculateDeltaConfigForm.get("periodValueKey").updateValueAndValidity({emitEvent:e})}validatorTriggers(){return["addPeriodBetweenMsgs"]}}e("CalculateDeltaConfigComponent",Fn),Fn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Fn,deps:[{token:G.Store},{token:H.TranslateService},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Fn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Fn,selector:"tb-enrichment-node-calculate-delta-config",usesInheritance:!0,ngImport:t,template:"
\n
\n \n {{ 'tb.rulenode.input-value-key' | translate }}\n \n \n {{ 'tb.rulenode.input-value-key-required' | translate }}\n \n \n \n {{ 'tb.rulenode.output-value-key' | translate }}\n \n \n {{ 'tb.rulenode.output-value-key-required' | translate }}\n \n \n
\n \n {{ 'tb.rulenode.number-of-digits-after-floating-point' | translate }}\n \n \n {{ 'tb.rulenode.number-of-digits-after-floating-point-range' | translate }}\n \n \n {{ 'tb.rulenode.number-of-digits-after-floating-point-range' | translate }}\n \n \n
\n
\n \n {{ 'tb.rulenode.failure-if-delta-negative' | translate }}\n \n
\n
\n \n {{ 'tb.rulenode.use-caching' | translate }}\n \n
\n
\n
\n \n {{ 'tb.rulenode.add-time-difference-between-readings' | translate:\n { inputValueKey: calculateDeltaConfigForm.get('inputValueKey').valid ?\n calculateDeltaConfigForm.get('inputValueKey').value : 'tb.rulenode.input-value-key' | translate } }}\n \n
\n \n {{ 'tb.rulenode.period-value-key' | translate }}\n \n \n {{ 'tb.rulenode.period-value-key-required' | translate }}\n \n \n
\n
\n
\n",dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:Ee.MatSlideToggle,selector:"mat-slide-toggle",inputs:["disabled","disableRipple","color","tabIndex"],exportAs:["matSlideToggle"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:De.HintTooltipIconComponent,selector:"[tb-hint-tooltip-icon]",inputs:["tb-hint-tooltip-icon","tooltipPosition","hintIcon"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Fn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-calculate-delta-config",template:"
\n
\n \n {{ 'tb.rulenode.input-value-key' | translate }}\n \n \n {{ 'tb.rulenode.input-value-key-required' | translate }}\n \n \n \n {{ 'tb.rulenode.output-value-key' | translate }}\n \n \n {{ 'tb.rulenode.output-value-key-required' | translate }}\n \n \n
\n \n {{ 'tb.rulenode.number-of-digits-after-floating-point' | translate }}\n \n \n {{ 'tb.rulenode.number-of-digits-after-floating-point-range' | translate }}\n \n \n {{ 'tb.rulenode.number-of-digits-after-floating-point-range' | translate }}\n \n \n
\n
\n \n {{ 'tb.rulenode.failure-if-delta-negative' | translate }}\n \n
\n
\n \n {{ 'tb.rulenode.use-caching' | translate }}\n \n
\n
\n
\n \n {{ 'tb.rulenode.add-time-difference-between-readings' | translate:\n { inputValueKey: calculateDeltaConfigForm.get('inputValueKey').valid ?\n calculateDeltaConfigForm.get('inputValueKey').value : 'tb.rulenode.input-value-key' | translate } }}\n \n
\n \n {{ 'tb.rulenode.period-value-key' | translate }}\n \n \n {{ 'tb.rulenode.period-value-key-required' | translate }}\n \n \n
\n
\n
\n"}]}],ctorParameters:function(){return[{type:G.Store},{type:H.TranslateService},{type:E.FormBuilder}]}});class kn extends s{constructor(e,t,n){super(e),this.store=e,this.fb=t,this.translate=n,this.fetchToData=[],this.DataToFetch=ht;for(const e of vt.keys())e!==ht.FIELDS&&this.fetchToData.push({value:e,name:this.translate.instant(vt.get(e))})}configForm(){return this.customerAttributesConfigForm}prepareOutputConfig(e){const t={};for(const n of Object.keys(e.dataMapping))t[n.trim()]=e.dataMapping[n];return e.dataMapping=t,W(e)}prepareInputConfig(e){let t,n;return t=Y(e?.telemetry)?e.telemetry?ht.LATEST_TELEMETRY:ht.ATTRIBUTES:Y(e?.dataToFetch)?e.dataToFetch:ht.ATTRIBUTES,n=Y(e?.attrMapping)?e.attrMapping:Y(e?.dataMapping)?e.dataMapping:null,{dataToFetch:t,dataMapping:n,fetchTo:Y(e?.fetchTo)?e.fetchTo:Pt.METADATA}}selectTranslation(e,t){return this.customerAttributesConfigForm.get("dataToFetch").value===ht.LATEST_TELEMETRY?e:t}onConfigurationSet(e){this.customerAttributesConfigForm=this.fb.group({dataToFetch:[e.dataToFetch,[]],dataMapping:[e.dataMapping,[D.required]],fetchTo:[e.fetchTo]})}}e("CustomerAttributesConfigComponent",kn),kn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:kn,deps:[{token:G.Store},{token:E.FormBuilder},{token:H.TranslateService}],target:t.ɵɵFactoryTarget.Component}),kn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:kn,selector:"tb-enrichment-node-customer-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n
tb.rulenode.mapping-of-customers-fields
\n
\n
\n \n \n {{ data.name }}\n \n \n
\n
\n \n \n \n \n
\n',styles:[":host .fetch-to-data-toggle{max-width:420px;width:100%}\n"],dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:_.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"directive",type:Oe.ToggleOption,selector:"tb-toggle-option",inputs:["value"]},{kind:"component",type:Be.ToggleSelectComponent,selector:"tb-toggle-select",inputs:["disabled","selectMediaBreakpoint","appearance","disablePagination"]},{kind:"component",type:un,selector:"tb-kv-map-config",inputs:["disabled","uniqueKeyValuePairValidator","labelText","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","popupHelpLink","required"]},{kind:"component",type:gn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:kn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-customer-attributes-config",template:'
\n
tb.rulenode.mapping-of-customers-fields
\n
\n
\n \n \n {{ data.name }}\n \n \n
\n
\n \n \n \n \n
\n',styles:[":host .fetch-to-data-toggle{max-width:420px;width:100%}\n"]}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder},{type:H.TranslateService}]}});class Ln extends s{constructor(e,t,n){super(e),this.store=e,this.translate=t,this.fb=n}configForm(){return this.deviceAttributesConfigForm}onConfigurationSet(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e.deviceRelationsQuery,[D.required]],tellFailureIfAbsent:[e.tellFailureIfAbsent,[]],fetchTo:[e.fetchTo,[]],attributesControl:[e.attributesControl,[]]})}touched(){this.deviceAttributesConfigForm.get("attributesControl").markAsTouched()}prepareInputConfig(e){return X(e)&&(e.attributesControl={clientAttributeNames:Y(e?.clientAttributeNames)?e.clientAttributeNames:null,latestTsKeyNames:Y(e?.latestTsKeyNames)?e.latestTsKeyNames:null,serverAttributeNames:Y(e?.serverAttributeNames)?e.serverAttributeNames:null,sharedAttributeNames:Y(e?.sharedAttributeNames)?e.sharedAttributeNames:null,getLatestValueWithTs:!!Y(e?.getLatestValueWithTs)&&e.getLatestValueWithTs}),{deviceRelationsQuery:Y(e?.deviceRelationsQuery)?e.deviceRelationsQuery:null,tellFailureIfAbsent:!Y(e?.tellFailureIfAbsent)||e.tellFailureIfAbsent,fetchTo:Y(e?.fetchTo)?e.fetchTo:Pt.METADATA,attributesControl:e?e.attributesControl:null}}prepareOutputConfig(e){for(const t of Object.keys(e.attributesControl))e[t]=e.attributesControl[t];return delete e.attributesControl,e}}e("DeviceAttributesConfigComponent",Ln),Ln.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ln,deps:[{token:G.Store},{token:H.TranslateService},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ln.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Ln,selector:"tb-enrichment-node-device-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n
\n
tb.rulenode.device-relations-query
\n \n \n
\n
\n
\n
tb.rulenode.related-device-attributes
\n
\n tb.rulenode.at-least-one-field-required\n
\n
\n \n \n
\n
\n \n {{ \'tb.rulenode.tell-failure\' | translate }}\n \n
\n
\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"component",type:Ee.MatSlideToggle,selector:"mat-slide-toggle",inputs:["disabled","disableRipple","color","tabIndex"],exportAs:["matSlideToggle"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:De.HintTooltipIconComponent,selector:"[tb-hint-tooltip-icon]",inputs:["tb-hint-tooltip-icon","tooltipPosition","hintIcon"]},{kind:"component",type:pn,selector:"tb-device-relations-query-config",inputs:["disabled","required"]},{kind:"component",type:gn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"component",type:bn,selector:"tb-select-attributes",inputs:["popupHelpLink"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Ln,decorators:[{type:n,args:[{selector:"tb-enrichment-node-device-attributes-config",template:'
\n
\n
tb.rulenode.device-relations-query
\n \n \n
\n
\n
\n
tb.rulenode.related-device-attributes
\n
\n tb.rulenode.at-least-one-field-required\n
\n
\n \n \n
\n
\n \n {{ \'tb.rulenode.tell-failure\' | translate }}\n \n
\n
\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:H.TranslateService},{type:E.FormBuilder}]}});class Tn extends s{constructor(e,t,n){super(e),this.store=e,this.translate=t,this.fb=n,this.predefinedValues=[];for(const e of Object.keys(ct))this.predefinedValues.push({value:ct[e],name:this.translate.instant(ft.get(ct[e]))})}ngOnInit(){super.ngOnInit()}configForm(){return this.entityDetailsConfigForm}prepareInputConfig(e){let t;return t=Y(e?.addToMetadata)?e.addToMetadata?Pt.METADATA:Pt.DATA:e?.fetchTo?e.fetchTo:Pt.DATA,{detailsList:Y(e?.detailsList)?e.detailsList:null,fetchTo:t}}onConfigurationSet(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e.detailsList,[D.required]],fetchTo:[e.fetchTo,[]]})}}e("EntityDetailsConfigComponent",Tn),Tn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Tn,deps:[{token:G.Store},{token:H.TranslateService},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Tn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Tn,selector:"tb-enrichment-node-entity-details-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n help\n \n \n \n \n
\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"component",type:Re.StringItemsListComponent,selector:"tb-string-items-list",inputs:["required","disabled","label","placeholder","hint","requiredText","floatLabel","appearance","editable","subscriptSizing","predefinedValues"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:gn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Tn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-entity-details-config",template:'
\n \n \n help\n \n \n \n \n
\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:H.TranslateService},{type:E.FormBuilder}]}});class In extends s{constructor(e,t,n){super(e),this.store=e,this.translate=t,this.fb=n,this.separatorKeysCodes=[ae,ie,le],this.aggregationTypes=I,this.aggregations=Object.values(I),this.aggregationTypesTranslations=N,this.fetchMode=gt,this.samplingOrders=Object.values(bt),this.samplingOrdersTranslate=Ft,this.timeUnits=Object.values(mt),this.timeUnitsTranslationMap=ut,this.deduplicationStrategiesHintTranslations=xt,this.headerOptions=[],this.timeUnitMap={[mt.MILLISECONDS]:1,[mt.SECONDS]:1e3,[mt.MINUTES]:6e4,[mt.HOURS]:36e5,[mt.DAYS]:864e5},this.intervalValidator=()=>e=>e.get("startInterval").value*this.timeUnitMap[e.get("startIntervalTimeUnit").value]<=e.get("endInterval").value*this.timeUnitMap[e.get("endIntervalTimeUnit").value]?{intervalError:!0}:null;for(const e of yt.keys())this.headerOptions.push({value:e,name:this.translate.instant(yt.get(e))})}configForm(){return this.getTelemetryFromDatabaseConfigForm}onConfigurationSet(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e.latestTsKeyNames,[D.required]],aggregation:[e.aggregation,[D.required]],fetchMode:[e.fetchMode,[D.required]],orderBy:[e.orderBy,[]],limit:[e.limit,[]],useMetadataIntervalPatterns:[e.useMetadataIntervalPatterns,[]],interval:this.fb.group({startInterval:[e.interval.startInterval,[]],startIntervalTimeUnit:[e.interval.startIntervalTimeUnit,[]],endInterval:[e.interval.endInterval,[]],endIntervalTimeUnit:[e.interval.endIntervalTimeUnit,[]]}),startIntervalPattern:[e.startIntervalPattern,[]],endIntervalPattern:[e.endIntervalPattern,[]]})}validatorTriggers(){return["fetchMode","useMetadataIntervalPatterns"]}toggleChange(e){this.getTelemetryFromDatabaseConfigForm.get("fetchMode").patchValue(e,{emitEvent:!0})}prepareOutputConfig(e){return e.startInterval=e.interval.startInterval,e.startIntervalTimeUnit=e.interval.startIntervalTimeUnit,e.endInterval=e.interval.endInterval,e.endIntervalTimeUnit=e.interval.endIntervalTimeUnit,delete e.interval,W(e)}prepareInputConfig(e){return X(e)&&(e.interval={startInterval:e.startInterval,startIntervalTimeUnit:e.startIntervalTimeUnit,endInterval:e.endInterval,endIntervalTimeUnit:e.endIntervalTimeUnit}),{latestTsKeyNames:Y(e?.latestTsKeyNames)?e.latestTsKeyNames:null,aggregation:Y(e?.aggregation)?e.aggregation:I.NONE,fetchMode:Y(e?.fetchMode)?e.fetchMode:gt.FIRST,orderBy:Y(e?.orderBy)?e.orderBy:bt.ASC,limit:Y(e?.limit)?e.limit:1e3,useMetadataIntervalPatterns:!!Y(e?.useMetadataIntervalPatterns)&&e.useMetadataIntervalPatterns,interval:{startInterval:Y(e?.interval?.startInterval)?e.interval.startInterval:2,startIntervalTimeUnit:Y(e?.interval?.startIntervalTimeUnit)?e.interval.startIntervalTimeUnit:mt.MINUTES,endInterval:Y(e?.interval?.endInterval)?e.interval.endInterval:1,endIntervalTimeUnit:Y(e?.interval?.endIntervalTimeUnit)?e.interval.endIntervalTimeUnit:mt.MINUTES},startIntervalPattern:Y(e?.startIntervalPattern)?e.startIntervalPattern:null,endIntervalPattern:Y(e?.endIntervalPattern)?e.endIntervalPattern:null}}updateValidators(e){const t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,n=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===gt.ALL?(this.getTelemetryFromDatabaseConfigForm.get("aggregation").setValidators([D.required]),this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([D.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([D.required,D.min(2),D.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("aggregation").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),n?(this.getTelemetryFromDatabaseConfigForm.get("interval.startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("interval.startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("interval.endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("interval.endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("interval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([D.required,D.pattern(/(?:.|\s)*\S(&:.|\s)*/)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([D.required,D.pattern(/(?:.|\s)*\S(&:.|\s)*/)])):(this.getTelemetryFromDatabaseConfigForm.get("interval.startInterval").setValidators([D.required,D.min(1),D.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("interval.startIntervalTimeUnit").setValidators([D.required]),this.getTelemetryFromDatabaseConfigForm.get("interval.endInterval").setValidators([D.required,D.min(1),D.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("interval.endIntervalTimeUnit").setValidators([D.required]),this.getTelemetryFromDatabaseConfigForm.get("interval").setValidators([this.intervalValidator()]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([])),this.getTelemetryFromDatabaseConfigForm.get("aggregation").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("orderBy").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("limit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("interval.startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("interval.startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("interval.endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("interval.endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("interval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})}removeKey(e,t){const n=this.getTelemetryFromDatabaseConfigForm.get(t).value,r=n.indexOf(e);r>=0&&(n.splice(r,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(n,{emitEvent:!0}))}clearChipGrid(){this.getTelemetryFromDatabaseConfigForm.get("latestTsKeyNames").patchValue([],{emitEvent:!0})}addKey(e,t){const n=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.getTelemetryFromDatabaseConfigForm.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(e,{emitEvent:!0}))}n&&(n.value="")}defaultPaddingEnable(){return this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value===gt.ALL&&this.getTelemetryFromDatabaseConfigForm.get("aggregation").value===I.NONE}}e("GetTelemetryFromDatabaseConfigComponent",In),In.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:In,deps:[{token:G.Store},{token:H.TranslateService},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),In.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:In,selector:"tb-enrichment-node-get-telemetry-from-database",usesInheritance:!0,ngImport:t,template:'
\n \n
\n help\n \n
\n
\n
tb.rulenode.fetch-interval
\n
\n \n {{ \'tb.rulenode.use-metadata-dynamic-interval\' | translate }}\n \n
\n
\n
\n \n {{ \'tb.rulenode.interval-start\' | translate }}\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n {{ \'tb.rulenode.time-unit\' | translate }}\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n {{ \'tb.rulenode.interval-end\' | translate }}\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n {{ \'tb.rulenode.time-unit\' | translate }}\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n error_outline\n
\n \n {{ \'tb.rulenode.fetch-timeseries-from-to\' | translate:\n {\n startInterval: getTelemetryFromDatabaseConfigForm.get(\'interval.startInterval\').value,\n endInterval: getTelemetryFromDatabaseConfigForm.get(\'interval.endInterval\').value,\n startIntervalTimeUnit: getTelemetryFromDatabaseConfigForm.get(\'interval.startIntervalTimeUnit\').value.toLowerCase(),\n endIntervalTimeUnit: getTelemetryFromDatabaseConfigForm.get(\'interval.endIntervalTimeUnit\').value.toLowerCase()\n } }}\n \n \n {{ "tb.rulenode.fetch-timeseries-from-to-invalid" | translate }}\n \n
\n
\n
\n \n
\n \n {{ \'tb.rulenode.start-interval\' | translate }}\n \n \n {{ \'tb.rulenode.start-interval-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.end-interval\' | translate }}\n \n \n {{ \'tb.rulenode.end-interval-required\' | translate }}\n \n \n \n \n
\n
\n
\n
\n
tb.rulenode.fetch-strategy
\n
\n
\n \n \n {{ data.name }}\n \n \n
\n
\n {{ deduplicationStrategiesHintTranslations.get(getTelemetryFromDatabaseConfigForm.get(\'fetchMode\').value) | translate }}\n
\n
\n
\n \n {{ \'aggregation.function\' | translate }}\n \n \n {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }}\n \n \n \n
\n \n {{ "tb.rulenode.order-by-timestamp" | translate }} \n \n \n {{ samplingOrdersTranslate.get(order) | translate }}\n \n \n \n \n {{ "tb.rulenode.limit" | translate }}\n \n {{ "tb.rulenode.limit-hint" | translate }}\n \n {{ \'tb.rulenode.limit-required\' | translate }}\n \n \n {{ \'tb.rulenode.limit-range\' | translate }}\n \n \n {{ \'tb.rulenode.limit-range\' | translate }}\n \n \n
\n
\n
\n
\n',styles:[":host .see-example{display:inline-block}:host .tb-hint{padding-bottom:0}:host .description-block{display:flex;align-items:center;background:#F7F7F7;border-radius:6px;border:1px solid #EAEAEA}:host .description-block .description-icon{font-size:24px;height:24px;min-height:24px;width:24px;min-width:24px;line-height:24px;color:#d9d9d9;margin:4px}:host .description-block .description-text{font-size:12px;line-height:16px;letter-spacing:.25px;margin:6px}:host .description-block.error{background:#FDF7F7;color:var(--mdc-theme-error, #f44336)}:host .description-block.error .description-icon{color:var(--mdc-theme-error, #f44336)}:host .item-center{align-items:center}:host .item-center .fetch-mod-toggle{max-width:630px;width:100%}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:Ae.HelpPopupComponent,selector:"[tb-help-popup], [tb-help-popup-content]",inputs:["tb-help-popup","tb-help-popup-content","trigger-text","trigger-style","tb-help-popup-placement","tb-help-popup-style","hintMode"]},{kind:"component",type:Re.StringItemsListComponent,selector:"tb-string-items-list",inputs:["required","disabled","label","placeholder","hint","requiredText","floatLabel","appearance","editable","subscriptSizing","predefinedValues"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"component",type:Ee.MatSlideToggle,selector:"mat-slide-toggle",inputs:["disabled","disableRipple","color","tabIndex"],exportAs:["matSlideToggle"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:E.FormGroupName,selector:"[formGroupName]",inputs:["formGroupName"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"directive",type:Oe.ToggleOption,selector:"tb-toggle-option",inputs:["value"]},{kind:"component",type:Be.ToggleSelectComponent,selector:"tb-toggle-select",inputs:["disabled","selectMediaBreakpoint","appearance","disablePagination"]},{kind:"component",type:De.HintTooltipIconComponent,selector:"[tb-hint-tooltip-icon]",inputs:["tb-hint-tooltip-icon","tooltipPosition","hintIcon"]},{kind:"component",type:mn,selector:"tb-example-hint",inputs:["hintText","popupHelpLink"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:In,decorators:[{type:n,args:[{selector:"tb-enrichment-node-get-telemetry-from-database",template:'
\n \n
\n help\n \n
\n
\n
tb.rulenode.fetch-interval
\n
\n \n {{ \'tb.rulenode.use-metadata-dynamic-interval\' | translate }}\n \n
\n
\n
\n \n {{ \'tb.rulenode.interval-start\' | translate }}\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n {{ \'tb.rulenode.time-unit\' | translate }}\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n {{ \'tb.rulenode.interval-end\' | translate }}\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n {{ \'tb.rulenode.time-unit\' | translate }}\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n error_outline\n
\n \n {{ \'tb.rulenode.fetch-timeseries-from-to\' | translate:\n {\n startInterval: getTelemetryFromDatabaseConfigForm.get(\'interval.startInterval\').value,\n endInterval: getTelemetryFromDatabaseConfigForm.get(\'interval.endInterval\').value,\n startIntervalTimeUnit: getTelemetryFromDatabaseConfigForm.get(\'interval.startIntervalTimeUnit\').value.toLowerCase(),\n endIntervalTimeUnit: getTelemetryFromDatabaseConfigForm.get(\'interval.endIntervalTimeUnit\').value.toLowerCase()\n } }}\n \n \n {{ "tb.rulenode.fetch-timeseries-from-to-invalid" | translate }}\n \n
\n
\n
\n \n
\n \n {{ \'tb.rulenode.start-interval\' | translate }}\n \n \n {{ \'tb.rulenode.start-interval-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.end-interval\' | translate }}\n \n \n {{ \'tb.rulenode.end-interval-required\' | translate }}\n \n \n \n \n
\n
\n
\n
\n
tb.rulenode.fetch-strategy
\n
\n
\n \n \n {{ data.name }}\n \n \n
\n
\n {{ deduplicationStrategiesHintTranslations.get(getTelemetryFromDatabaseConfigForm.get(\'fetchMode\').value) | translate }}\n
\n
\n
\n \n {{ \'aggregation.function\' | translate }}\n \n \n {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }}\n \n \n \n
\n \n {{ "tb.rulenode.order-by-timestamp" | translate }} \n \n \n {{ samplingOrdersTranslate.get(order) | translate }}\n \n \n \n \n {{ "tb.rulenode.limit" | translate }}\n \n {{ "tb.rulenode.limit-hint" | translate }}\n \n {{ \'tb.rulenode.limit-required\' | translate }}\n \n \n {{ \'tb.rulenode.limit-range\' | translate }}\n \n \n {{ \'tb.rulenode.limit-range\' | translate }}\n \n \n
\n
\n
\n
\n',styles:[":host .see-example{display:inline-block}:host .tb-hint{padding-bottom:0}:host .description-block{display:flex;align-items:center;background:#F7F7F7;border-radius:6px;border:1px solid #EAEAEA}:host .description-block .description-icon{font-size:24px;height:24px;min-height:24px;width:24px;min-width:24px;line-height:24px;color:#d9d9d9;margin:4px}:host .description-block .description-text{font-size:12px;line-height:16px;letter-spacing:.25px;margin:6px}:host .description-block.error{background:#FDF7F7;color:var(--mdc-theme-error, #f44336)}:host .description-block.error .description-icon{color:var(--mdc-theme-error, #f44336)}:host .item-center{align-items:center}:host .item-center .fetch-mod-toggle{max-width:630px;width:100%}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:H.TranslateService},{type:E.FormBuilder}]}});class Nn extends s{constructor(e,t,n){super(e),this.store=e,this.translate=t,this.fb=n}configForm(){return this.originatorAttributesConfigForm}touched(){this.originatorAttributesConfigForm.get("attributesControl").markAsTouched()}onConfigurationSet(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[e.tellFailureIfAbsent,[]],fetchTo:[e.fetchTo,[]],attributesControl:[e.attributesControl,[]]})}prepareInputConfig(e){return X(e)&&(e.attributesControl={clientAttributeNames:Y(e?.clientAttributeNames)?e.clientAttributeNames:null,latestTsKeyNames:Y(e?.latestTsKeyNames)?e.latestTsKeyNames:null,serverAttributeNames:Y(e?.serverAttributeNames)?e.serverAttributeNames:null,sharedAttributeNames:Y(e?.sharedAttributeNames)?e.sharedAttributeNames:null,getLatestValueWithTs:!!Y(e?.getLatestValueWithTs)&&e.getLatestValueWithTs}),{fetchTo:Y(e?.fetchTo)?e.fetchTo:Pt.METADATA,tellFailureIfAbsent:!!Y(e?.tellFailureIfAbsent)&&e.tellFailureIfAbsent,attributesControl:Y(e?.attributesControl)?e.attributesControl:null}}prepareOutputConfig(e){for(const t of Object.keys(e.attributesControl))e[t]=e.attributesControl[t];return delete e.attributesControl,e}}e("OriginatorAttributesConfigComponent",Nn),Nn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Nn,deps:[{token:G.Store},{token:H.TranslateService},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Nn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Nn,selector:"tb-enrichment-node-originator-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n
\n
\n
tb.rulenode.originator-attributes
\n
\n tb.rulenode.at-least-one-field-required\n
\n
\n \n \n \n \n
\n
\n \n {{ \'tb.rulenode.tell-failure\' | translate }}\n \n
\n
\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"component",type:Ee.MatSlideToggle,selector:"mat-slide-toggle",inputs:["disabled","disableRipple","color","tabIndex"],exportAs:["matSlideToggle"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:De.HintTooltipIconComponent,selector:"[tb-hint-tooltip-icon]",inputs:["tb-hint-tooltip-icon","tooltipPosition","hintIcon"]},{kind:"component",type:gn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"component",type:bn,selector:"tb-select-attributes",inputs:["popupHelpLink"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Nn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-originator-attributes-config",template:'
\n
\n
\n
tb.rulenode.originator-attributes
\n
\n tb.rulenode.at-least-one-field-required\n
\n
\n \n \n \n \n
\n
\n \n {{ \'tb.rulenode.tell-failure\' | translate }}\n \n
\n
\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:H.TranslateService},{type:E.FormBuilder}]}});class Sn extends s{constructor(e,t,n){super(e),this.store=e,this.fb=t,this.translate=n,this.originatorFields=[];for(const e of Object.keys(S))this.originatorFields.push({value:S[e].value,name:this.translate.instant(S[e].name)})}configForm(){return this.originatorFieldsConfigForm}prepareOutputConfig(e){return W(e)}prepareInputConfig(e){return{dataMapping:Y(e?.dataMapping)?e.dataMapping:null,ignoreNullStrings:Y(e?.ignoreNullStrings)?e.ignoreNullStrings:null,fetchTo:Y(e?.fetchTo)?e.fetchTo:Pt.METADATA}}onConfigurationSet(e){this.originatorFieldsConfigForm=this.fb.group({dataMapping:[e.dataMapping,[D.required]],ignoreNullStrings:[e.ignoreNullStrings,[]],fetchTo:[e.fetchTo,[]]})}}e("OriginatorFieldsConfigComponent",Sn),Sn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Sn,deps:[{token:G.Store},{token:E.FormBuilder},{token:H.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Sn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Sn,selector:"tb-enrichment-node-originator-fields-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n
\n \n {{ \'tb.rulenode.skip-empty-fields\' | translate }}\n \n
\n
\n',dependencies:[{kind:"component",type:Ee.MatSlideToggle,selector:"mat-slide-toggle",inputs:["disabled","disableRipple","color","tabIndex"],exportAs:["matSlideToggle"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:De.HintTooltipIconComponent,selector:"[tb-hint-tooltip-icon]",inputs:["tb-hint-tooltip-icon","tooltipPosition","hintIcon"]},{kind:"component",type:gn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"component",type:yn,selector:"tb-sv-map-config",inputs:["selectOptions","disabled","labelText","requiredText","targetKeyPrefix","selectText","selectRequiredText","valText","valRequiredText","hintText","popupHelpLink","required"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Sn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-originator-fields-config",template:'
\n \n \n \n \n
\n \n {{ \'tb.rulenode.skip-empty-fields\' | translate }}\n \n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder},{type:H.TranslateService}]}});class qn extends s{constructor(e,t,n){super(e),this.store=e,this.fb=t,this.translate=n,this.DataToFetch=ht,this.msgMetadataLabelTranslations=Ct,this.originatorFields=[],this.fetchToData=[],this.destroy$=new Se;for(const e of Object.keys(S))this.originatorFields.push({value:S[e].value,name:this.translate.instant(S[e].name)});for(const e of vt.keys())this.fetchToData.push({value:e,name:this.translate.instant(vt.get(e))})}configForm(){return this.relatedAttributesConfigForm}prepareOutputConfig(e){e.dataToFetch===ht.FIELDS?(e.dataMapping=e.svMap,delete e.svMap):(e.dataMapping=e.kvMap,delete e.kvMap);const t={};for(const n of Object.keys(e.dataMapping))t[n.trim()]=e.dataMapping[n];return e.dataMapping=t,delete e.svMap,delete e.kvMap,W(e)}prepareInputConfig(e){let t,n,r={[S.name.value]:`relatedEntity${this.translate.instant(S.name.name)}`},o={serialNumber:"sn"};return t=Y(e?.telemetry)?e.telemetry?ht.LATEST_TELEMETRY:ht.ATTRIBUTES:Y(e?.dataToFetch)?e.dataToFetch:ht.ATTRIBUTES,n=Y(e?.attrMapping)?e.attrMapping:Y(e?.dataMapping)?e.dataMapping:null,t===ht.FIELDS?r=n:o=n,{relationsQuery:Y(e?.relationsQuery)?e.relationsQuery:null,dataToFetch:t,svMap:r,kvMap:o,fetchTo:Y(e?.fetchTo)?e.fetchTo:Pt.METADATA}}selectTranslation(e,t){return this.relatedAttributesConfigForm.get("dataToFetch").value===ht.LATEST_TELEMETRY?e:t}onConfigurationSet(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e.relationsQuery,[D.required]],dataToFetch:[e.dataToFetch,[]],kvMap:[e.kvMap,[]],svMap:[e.svMap,[]],fetchTo:[e.fetchTo,[]]}),this.relatedAttributesConfigForm.get("dataToFetch").valueChanges.pipe(Fe(this.destroy$)).subscribe((e=>{e===ht.FIELDS?(this.relatedAttributesConfigForm.get("svMap").setValidators(D.required),this.relatedAttributesConfigForm.get("svMap").updateValueAndValidity(),this.relatedAttributesConfigForm.get("kvMap").clearValidators(),this.relatedAttributesConfigForm.get("kvMap").updateValueAndValidity()):(this.relatedAttributesConfigForm.get("kvMap").setValidators(D.required),this.relatedAttributesConfigForm.get("kvMap").updateValueAndValidity(),this.relatedAttributesConfigForm.get("svMap").clearValidators(),this.relatedAttributesConfigForm.get("svMap").updateValueAndValidity())}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}}e("RelatedAttributesConfigComponent",qn),qn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:qn,deps:[{token:G.Store},{token:E.FormBuilder},{token:H.TranslateService}],target:t.ɵɵFactoryTarget.Component}),qn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:qn,selector:"tb-enrichment-node-related-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n
tb.rulenode.data-to-fetch
\n
\n
\n \n \n {{ data.name }}\n \n \n
\n
\n \n \n \n \n \n \n \n \n
\n
\n',styles:[":host .toggle-block{display:flex;place-content:center}:host .toggle-block .fetch-to-data-toggle{max-width:630px;width:100%}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"directive",type:Oe.ToggleOption,selector:"tb-toggle-option",inputs:["value"]},{kind:"component",type:Be.ToggleSelectComponent,selector:"tb-toggle-select",inputs:["disabled","selectMediaBreakpoint","appearance","disablePagination"]},{kind:"component",type:un,selector:"tb-kv-map-config",inputs:["disabled","uniqueKeyValuePairValidator","labelText","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","popupHelpLink","required"]},{kind:"component",type:dn,selector:"tb-relations-query-config",inputs:["disabled","required"]},{kind:"component",type:gn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"component",type:yn,selector:"tb-sv-map-config",inputs:["selectOptions","disabled","labelText","requiredText","targetKeyPrefix","selectText","selectRequiredText","valText","valRequiredText","hintText","popupHelpLink","required"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:qn,decorators:[{type:n,args:[{selector:"tb-enrichment-node-related-attributes-config",template:'
\n \n \n
\n
tb.rulenode.data-to-fetch
\n
\n
\n \n \n {{ data.name }}\n \n \n
\n
\n \n \n \n \n \n \n \n \n
\n
\n',styles:[":host .toggle-block{display:flex;place-content:center}:host .toggle-block .fetch-to-data-toggle{max-width:630px;width:100%}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder},{type:H.TranslateService}]}});class An extends s{constructor(e,t,n){super(e),this.store=e,this.fb=t,this.translate=n,this.fetchToData=[],this.DataToFetch=ht;for(const e of vt.keys())e!==ht.FIELDS&&this.fetchToData.push({value:e,name:this.translate.instant(vt.get(e))})}configForm(){return this.tenantAttributesConfigForm}prepareInputConfig(e){let t,n;return t=Y(e?.telemetry)?e.telemetry?ht.LATEST_TELEMETRY:ht.ATTRIBUTES:Y(e?.dataToFetch)?e.dataToFetch:ht.ATTRIBUTES,n=Y(e?.attrMapping)?e.attrMapping:Y(e?.dataMapping)?e.dataMapping:null,{dataToFetch:t,dataMapping:n,fetchTo:Y(e?.fetchTo)?e.fetchTo:Pt.METADATA}}selectTranslation(e,t){return this.tenantAttributesConfigForm.get("dataToFetch").value===ht.LATEST_TELEMETRY?e:t}onConfigurationSet(e){this.tenantAttributesConfigForm=this.fb.group({dataToFetch:[e.dataToFetch,[]],dataMapping:[e.dataMapping,[D.required]],fetchTo:[e.fetchTo,[]]})}}e("TenantAttributesConfigComponent",An),An.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:An,deps:[{token:G.Store},{token:E.FormBuilder},{token:H.TranslateService}],target:t.ɵɵFactoryTarget.Component}),An.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:An,selector:"tb-enrichment-node-tenant-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n
tb.rulenode.mapping-of-tenant-fields
\n
\n
\n \n \n {{ data.name }}\n \n \n
\n
\n \n \n \n \n
\n',styles:[":host .fetch-to-data-toggle{max-width:420px;width:100%}\n"],dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:_.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"directive",type:Oe.ToggleOption,selector:"tb-toggle-option",inputs:["value"]},{kind:"component",type:Be.ToggleSelectComponent,selector:"tb-toggle-select",inputs:["disabled","selectMediaBreakpoint","appearance","disablePagination"]},{kind:"component",type:un,selector:"tb-kv-map-config",inputs:["disabled","uniqueKeyValuePairValidator","labelText","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","popupHelpLink","required"]},{kind:"component",type:gn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:An,decorators:[{type:n,args:[{selector:"tb-enrichment-node-tenant-attributes-config",template:'
\n
tb.rulenode.mapping-of-tenant-fields
\n
\n
\n \n \n {{ data.name }}\n \n \n
\n
\n \n \n \n \n
\n',styles:[":host .fetch-to-data-toggle{max-width:420px;width:100%}\n"]}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder},{type:H.TranslateService}]}});class Mn extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.fetchDeviceCredentialsConfigForm}prepareInputConfig(e){return{fetchTo:Y(e?.fetchTo)?e.fetchTo:Pt.METADATA}}onConfigurationSet(e){this.fetchDeviceCredentialsConfigForm=this.fb.group({fetchTo:[e.fetchTo,[]]})}}e("FetchDeviceCredentialsConfigComponent",Mn),Mn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Mn,deps:[{token:G.Store},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Mn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Mn,selector:"./tb-enrichment-node-fetch-device-credentials-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n',dependencies:[{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:gn,selector:"tb-msg-metadata-chip",inputs:["labelText"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Mn,decorators:[{type:n,args:[{selector:"./tb-enrichment-node-fetch-device-credentials-config",template:'
\n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder}]}});class Gn{}e("RulenodeCoreConfigEnrichmentModule",Gn),Gn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Gn,deps:[],target:t.ɵɵFactoryTarget.NgModule}),Gn.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:Gn,declarations:[kn,Tn,Ln,Nn,Sn,In,qn,An,Fn,Mn],imports:[O,T,vn],exports:[kn,Tn,Ln,Nn,Sn,In,qn,An,Fn,Mn]}),Gn.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Gn,imports:[O,T,vn]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Gn,decorators:[{type:l,args:[{declarations:[kn,Tn,Ln,Nn,Sn,In,qn,An,Fn,Mn],imports:[O,T,vn],exports:[kn,Tn,Ln,Nn,Sn,In,qn,An,Fn,Mn]}]}]});class En extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.allAzureIotHubCredentialsTypes=Nt,this.azureIotHubCredentialsTypeTranslationsMap=St}configForm(){return this.azureIotHubConfigForm}onConfigurationSet(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[D.required]],host:[e?e.host:null,[D.required]],port:[e?e.port:null,[D.required,D.min(1),D.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[D.required,D.min(1),D.max(200)]],clientId:[e?e.clientId:null,[D.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[D.required]],sasKey:[e&&e.credentials?e.credentials.sasKey:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]]})})}prepareOutputConfig(e){const t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e}validatorTriggers(){return["credentials.type"]}updateValidators(e){const t=this.azureIotHubConfigForm.get("credentials"),n=t.get("type").value;switch(e&&t.reset({type:n},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),n){case"sas":t.get("sasKey").setValidators([D.required]);break;case"cert.PEM":t.get("privateKey").setValidators([D.required]),t.get("privateKeyFileName").setValidators([D.required]),t.get("cert").setValidators([D.required]),t.get("certFileName").setValidators([D.required])}t.get("sasKey").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})}}e("AzureIotHubConfigComponent",En),En.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:En,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),En.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:En,selector:"tb-external-node-azure-iot-hub-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}\n"],dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:R.NgSwitch,selector:"[ngSwitch]",inputs:["ngSwitch"]},{kind:"directive",type:R.NgSwitchCase,selector:"[ngSwitchCase]",inputs:["ngSwitchCase"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:Te.MatAccordion,selector:"mat-accordion",inputs:["multi","hideToggle","displayMode","togglePosition"],exportAs:["matAccordion"]},{kind:"component",type:Te.MatExpansionPanel,selector:"mat-expansion-panel",inputs:["disabled","expanded","hideToggle","togglePosition"],outputs:["opened","closed","expandedChange","afterExpand","afterCollapse"],exportAs:["matExpansionPanel"]},{kind:"component",type:Te.MatExpansionPanelHeader,selector:"mat-expansion-panel-header",inputs:["tabIndex","expandedHeight","collapsedHeight"]},{kind:"directive",type:Te.MatExpansionPanelTitle,selector:"mat-panel-title"},{kind:"directive",type:Te.MatExpansionPanelDescription,selector:"mat-panel-description"},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:E.FormGroupName,selector:"[formGroupName]",inputs:["formGroupName"]},{kind:"component",type:Pe.FileInputComponent,selector:"tb-file-input",inputs:["label","accept","noFileText","inputId","allowedExtensions","dropLabel","contentConvertFunction","required","requiredAsError","disabled","existingFileName","readAsBinary","workFromFileObj","multipleFile"],outputs:["fileNameChanged"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:we.TogglePasswordComponent,selector:"tb-toggle-password"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:En,decorators:[{type:n,args:[{selector:"tb-external-node-azure-iot-hub-config",template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}\n"]}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Dn extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.ackValues=["all","-1","0","1"],this.ToByteStandartCharsetTypesValues=At,this.ToByteStandartCharsetTypeTranslationMap=Mt}configForm(){return this.kafkaConfigForm}onConfigurationSet(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[D.required]],keyPattern:[e?e.keyPattern:null],bootstrapServers:[e?e.bootstrapServers:null,[D.required]],retries:[e?e.retries:null,[D.min(0)]],batchSize:[e?e.batchSize:null,[D.min(0)]],linger:[e?e.linger:null,[D.min(0)]],bufferMemory:[e?e.bufferMemory:null,[D.min(0)]],acks:[e?e.acks:null,[D.required]],keySerializer:[e?e.keySerializer:null,[D.required]],valueSerializer:[e?e.valueSerializer:null,[D.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})}validatorTriggers(){return["addMetadataKeyValuesAsKafkaHeaders"]}updateValidators(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([D.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})}}e("KafkaConfigComponent",Dn),Dn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Dn,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Dn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Dn,selector:"tb-external-node-kafka-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.key-pattern\n \n tb.rulenode.general-pattern-hint\n \n
tb.rulenode.key-pattern-hint
\n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Dn,decorators:[{type:n,args:[{selector:"tb-external-node-kafka-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.key-pattern\n \n tb.rulenode.general-pattern-hint\n \n
tb.rulenode.key-pattern-hint
\n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Vn extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.mqttConfigForm}onConfigurationSet(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[D.required]],host:[e?e.host:null,[D.required]],port:[e?e.port:null,[D.required,D.min(1),D.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[D.required,D.min(1),D.max(200)]],clientId:[e?e.clientId:null,[]],appendClientIdSuffix:[{value:!!e&&e.appendClientIdSuffix,disabled:!(e&&Z(e.clientId))},[]],cleanSession:[!!e&&e.cleanSession,[]],retainedMessage:[!!e&&e.retainedMessage,[]],ssl:[!!e&&e.ssl,[]],credentials:[e?e.credentials:null,[]]})}updateValidators(e){Z(this.mqttConfigForm.get("clientId").value)?this.mqttConfigForm.get("appendClientIdSuffix").enable({emitEvent:!1}):this.mqttConfigForm.get("appendClientIdSuffix").disable({emitEvent:!1}),this.mqttConfigForm.get("appendClientIdSuffix").updateValueAndValidity({emitEvent:e})}validatorTriggers(){return["clientId"]}}e("MqttConfigComponent",Vn),Vn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Vn,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Vn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Vn,selector:"tb-external-node-mqtt-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n {{\'tb.rulenode.client-id-hint\' | translate}}\n \n \n {{ \'tb.rulenode.append-client-id-suffix\' | translate }}\n \n
{{ "tb.rulenode.client-id-suffix-hint" | translate }}
\n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ "tb.rulenode.retained-message" | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}\n"],dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:fn,selector:"tb-credentials-config",inputs:["required","disableCertPemCredentials","passwordFieldRequired"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Vn,decorators:[{type:n,args:[{selector:"tb-external-node-mqtt-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n {{\'tb.rulenode.client-id-hint\' | translate}}\n \n \n {{ \'tb.rulenode.append-client-id-suffix\' | translate }}\n \n
{{ "tb.rulenode.client-id-suffix-hint" | translate }}
\n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ "tb.rulenode.retained-message" | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}\n"]}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Pn extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.notificationType=q,this.entityType=x}configForm(){return this.notificationConfigForm}onConfigurationSet(e){this.notificationConfigForm=this.fb.group({templateId:[e?e.templateId:null,[D.required]],targets:[e?e.targets:[],[D.required]]})}}e("NotificationConfigComponent",Pn),Pn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Pn,deps:[{token:G.Store},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Pn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Pn,selector:"tb-external-node-notification-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n
\n',dependencies:[{kind:"component",type:Ke.EntityListComponent,selector:"tb-entity-list",inputs:["entityType","subType","labelText","placeholderText","requiredText","required","disabled","subscriptSizing","hint"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:Ue.TemplateAutocompleteComponent,selector:"tb-template-autocomplete",inputs:["required","allowCreate","allowEdit","disabled","notificationTypes"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Pn,decorators:[{type:n,args:[{selector:"tb-external-node-notification-config",template:'
\n \n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder}]}});class wn extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.pubSubConfigForm}onConfigurationSet(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[D.required]],topicName:[e?e.topicName:null,[D.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[D.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[D.required]],messageAttributes:[e?e.messageAttributes:null,[]]})}}e("PubSubConfigComponent",wn),wn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:wn,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),wn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:wn,selector:"tb-external-node-pub-sub-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:Pe.FileInputComponent,selector:"tb-file-input",inputs:["label","accept","noFileText","inputId","allowedExtensions","dropLabel","contentConvertFunction","required","requiredAsError","disabled","existingFileName","readAsBinary","workFromFileObj","multipleFile"],outputs:["fileNameChanged"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:me.SafePipe,name:"safe"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:wn,decorators:[{type:n,args:[{selector:"tb-external-node-pub-sub-config",template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Rn extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"]}configForm(){return this.rabbitMqConfigForm}onConfigurationSet(e){this.rabbitMqConfigForm=this.fb.group({exchangeNamePattern:[e?e.exchangeNamePattern:null,[]],routingKeyPattern:[e?e.routingKeyPattern:null,[]],messageProperties:[e?e.messageProperties:null,[]],host:[e?e.host:null,[D.required]],port:[e?e.port:null,[D.required,D.min(1),D.max(65535)]],virtualHost:[e?e.virtualHost:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]],automaticRecoveryEnabled:[!!e&&e.automaticRecoveryEnabled,[]],connectionTimeout:[e?e.connectionTimeout:null,[D.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[D.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})}}e("RabbitMqConfigComponent",Rn),Rn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Rn,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Rn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Rn,selector:"tb-external-node-rabbit-mq-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:we.TogglePasswordComponent,selector:"tb-toggle-password"},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Rn,decorators:[{type:n,args:[{selector:"tb-external-node-rabbit-mq-config",template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class On extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.proxySchemes=["http","https"],this.httpRequestTypes=Object.keys(qt)}configForm(){return this.restApiCallConfigForm}onConfigurationSet(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[D.required]],requestMethod:[e?e.requestMethod:null,[D.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],trimDoubleQuotes:[!!e&&e.trimDoubleQuotes,[]],ignoreRequestBody:[!!e&&e.ignoreRequestBody,[]],enableProxy:[!!e&&e.enableProxy,[]],useSystemProxyProperties:[!!e&&e.enableProxy,[]],proxyScheme:[e?e.proxyHost:null,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],readTimeoutMs:[e?e.readTimeoutMs:null,[]],maxParallelRequestsCount:[e?e.maxParallelRequestsCount:null,[D.min(0)]],headers:[e?e.headers:null,[]],useRedisQueueForMsgPersistence:[!!e&&e.useRedisQueueForMsgPersistence,[]],trimQueue:[!!e&&e.trimQueue,[]],maxQueueSize:[e?e.maxQueueSize:null,[]],credentials:[e?e.credentials:null,[]]})}validatorTriggers(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]}updateValidators(e){const t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,n=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,r=this.restApiCallConfigForm.get("enableProxy").value,o=this.restApiCallConfigForm.get("useSystemProxyProperties").value;r&&!o?(this.restApiCallConfigForm.get("proxyHost").setValidators(r?[D.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(r?[D.required,D.min(1),D.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([D.min(0)])),n?this.restApiCallConfigForm.get("maxQueueSize").setValidators([D.min(0)]):this.restApiCallConfigForm.get("maxQueueSize").setValidators([]),this.restApiCallConfigForm.get("readTimeoutMs").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("maxQueueSize").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("credentials").updateValueAndValidity({emitEvent:e})}}e("RestApiCallConfigComponent",On),On.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:On,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),On.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:On,selector:"tb-external-node-rest-api-call-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n \n {{ \'tb.rulenode.trim-double-quotes\' | translate }}\n \n
tb.rulenode.trim-double-quotes-hint
\n \n {{ \'tb.rulenode.ignore-request-body\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n tb.rulenode.read-timeout-hint\n \n \n tb.rulenode.max-parallel-requests-count\n \n tb.rulenode.max-parallel-requests-count-hint\n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:fn,selector:"tb-credentials-config",inputs:["required","disableCertPemCredentials","passwordFieldRequired"]},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:me.SafePipe,name:"safe"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:On,decorators:[{type:n,args:[{selector:"tb-external-node-rest-api-call-config",template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n \n {{ \'tb.rulenode.trim-double-quotes\' | translate }}\n \n
tb.rulenode.trim-double-quotes-hint
\n \n {{ \'tb.rulenode.ignore-request-body\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n tb.rulenode.read-timeout-hint\n \n \n tb.rulenode.max-parallel-requests-count\n \n tb.rulenode.max-parallel-requests-count-hint\n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Bn extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.smtpProtocols=["smtp","smtps"],this.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"]}configForm(){return this.sendEmailConfigForm}onConfigurationSet(e){this.sendEmailConfigForm=this.fb.group({useSystemSmtpSettings:[!!e&&e.useSystemSmtpSettings,[]],smtpProtocol:[e?e.smtpProtocol:null,[]],smtpHost:[e?e.smtpHost:null,[]],smtpPort:[e?e.smtpPort:null,[]],timeout:[e?e.timeout:null,[]],enableTls:[!!e&&e.enableTls,[]],tlsVersion:[e?e.tlsVersion:null,[]],enableProxy:[!!e&&e.enableProxy,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]]})}validatorTriggers(){return["useSystemSmtpSettings","enableProxy"]}updateValidators(e){const t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,n=this.sendEmailConfigForm.get("enableProxy").value;t?(this.sendEmailConfigForm.get("smtpProtocol").setValidators([]),this.sendEmailConfigForm.get("smtpHost").setValidators([]),this.sendEmailConfigForm.get("smtpPort").setValidators([]),this.sendEmailConfigForm.get("timeout").setValidators([]),this.sendEmailConfigForm.get("proxyHost").setValidators([]),this.sendEmailConfigForm.get("proxyPort").setValidators([])):(this.sendEmailConfigForm.get("smtpProtocol").setValidators([D.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([D.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([D.required,D.min(1),D.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([D.required,D.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(n?[D.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(n?[D.required,D.min(1),D.max(65535)]:[])),this.sendEmailConfigForm.get("smtpProtocol").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpPort").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("timeout").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})}}e("SendEmailConfigComponent",Bn),Bn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Bn,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Bn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Bn,selector:"tb-external-node-send-email-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n
\n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:_e.TbCheckboxComponent,selector:"tb-checkbox",inputs:["disabled","trueValue","falseValue"],outputs:["valueChange"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:we.TogglePasswordComponent,selector:"tb-toggle-password"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Bn,decorators:[{type:n,args:[{selector:"tb-external-node-send-email-config",template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Kn extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.sendSmsConfigForm}onConfigurationSet(e){this.sendSmsConfigForm=this.fb.group({numbersToTemplate:[e?e.numbersToTemplate:null,[D.required]],smsMessageTemplate:[e?e.smsMessageTemplate:null,[D.required]],useSystemSmsSettings:[!!e&&e.useSystemSmsSettings,[]],smsProviderConfiguration:[e?e.smsProviderConfiguration:null,[]]})}validatorTriggers(){return["useSystemSmsSettings"]}updateValidators(e){this.sendSmsConfigForm.get("useSystemSmsSettings").value?this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([]):this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([D.required]),this.sendSmsConfigForm.get("smsProviderConfiguration").updateValueAndValidity({emitEvent:e})}}e("SendSmsConfigComponent",Kn),Kn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Kn,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Kn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Kn,selector:"tb-external-node-send-sms-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.numbers-to-template\n \n \n {{ \'tb.rulenode.numbers-to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.sms-message-template\n \n \n {{ \'tb.rulenode.sms-message-template-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n {{ \'tb.rulenode.use-system-sms-settings\' | translate }}\n \n \n \n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:He.SmsProviderConfigurationComponent,selector:"tb-sms-provider-configuration",inputs:["required","disabled"]},{kind:"pipe",type:me.SafePipe,name:"safe"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Kn,decorators:[{type:n,args:[{selector:"tb-external-node-send-sms-config",template:'
\n \n tb.rulenode.numbers-to-template\n \n \n {{ \'tb.rulenode.numbers-to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.sms-message-template\n \n \n {{ \'tb.rulenode.sms-message-template-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n {{ \'tb.rulenode.use-system-sms-settings\' | translate }}\n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Un extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.slackChanelTypes=Object.keys(A),this.slackChanelTypesTranslateMap=M}configForm(){return this.slackConfigForm}onConfigurationSet(e){this.slackConfigForm=this.fb.group({botToken:[e?e.botToken:null],useSystemSettings:[!!e&&e.useSystemSettings],messageTemplate:[e?e.messageTemplate:null,[D.required]],conversationType:[e?e.conversationType:null,[D.required]],conversation:[e?e.conversation:null,[D.required]]})}validatorTriggers(){return["useSystemSettings"]}updateValidators(e){this.slackConfigForm.get("useSystemSettings").value?this.slackConfigForm.get("botToken").clearValidators():this.slackConfigForm.get("botToken").setValidators([D.required]),this.slackConfigForm.get("botToken").updateValueAndValidity({emitEvent:e})}}e("SlackConfigComponent",Un),Un.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Un,deps:[{token:G.Store},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Un.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Un,selector:"tb-external-node-slack-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.message-template\n \n \n {{ \'tb.rulenode.message-template-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n {{ \'tb.rulenode.use-system-slack-settings\' | translate }}\n \n \n tb.rulenode.slack-api-token\n \n \n {{ \'tb.rulenode.slack-api-token-required\' | translate }}\n \n \n \n \n \n {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }}\n \n \n \n \n
\n',styles:[":host .tb-title{display:block;padding-bottom:6px}:host ::ng-deep .mat-mdc-radio-group{display:flex;flex-direction:row;margin-bottom:22px;gap:12px}:host ::ng-deep .mat-mdc-radio-group .mat-mdc-radio-button{flex:1 1 100%;padding:4px;border:1px solid rgba(0,0,0,.12);border-radius:6px}@media screen and (max-width: 599px){:host ::ng-deep .mat-mdc-radio-group{flex-direction:column}}\n"],dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:B.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex"],exportAs:["matCheckbox"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:ze.MatRadioGroup,selector:"mat-radio-group",exportAs:["matRadioGroup"]},{kind:"component",type:ze.MatRadioButton,selector:"mat-radio-button",inputs:["disableRipple","tabIndex"],exportAs:["matRadioButton"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:je.SlackConversationAutocompleteComponent,selector:"tb-slack-conversation-autocomplete",inputs:["labelText","requiredText","required","disabled","slackChanelType","token"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Un,decorators:[{type:n,args:[{selector:"tb-external-node-slack-config",template:'
\n \n tb.rulenode.message-template\n \n \n {{ \'tb.rulenode.message-template-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n {{ \'tb.rulenode.use-system-slack-settings\' | translate }}\n \n \n tb.rulenode.slack-api-token\n \n \n {{ \'tb.rulenode.slack-api-token-required\' | translate }}\n \n \n \n \n \n {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }}\n \n \n \n \n
\n',styles:[":host .tb-title{display:block;padding-bottom:6px}:host ::ng-deep .mat-mdc-radio-group{display:flex;flex-direction:row;margin-bottom:22px;gap:12px}:host ::ng-deep .mat-mdc-radio-group .mat-mdc-radio-button{flex:1 1 100%;padding:4px;border:1px solid rgba(0,0,0,.12);border-radius:6px}@media screen and (max-width: 599px){:host ::ng-deep .mat-mdc-radio-group{flex-direction:column}}\n"]}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder}]}});class _n extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.snsConfigForm}onConfigurationSet(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[D.required]],accessKeyId:[e?e.accessKeyId:null,[D.required]],secretAccessKey:[e?e.secretAccessKey:null,[D.required]],region:[e?e.region:null,[D.required]]})}}e("SnsConfigComponent",_n),_n.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:_n,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),_n.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:_n,selector:"tb-external-node-sns-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:_n,decorators:[{type:n,args:[{selector:"tb-external-node-sns-config",template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Hn extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.sqsQueueType=kt,this.sqsQueueTypes=Object.keys(kt),this.sqsQueueTypeTranslationsMap=Lt}configForm(){return this.sqsConfigForm}onConfigurationSet(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[D.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[D.required]],delaySeconds:[e?e.delaySeconds:null,[D.min(0),D.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[D.required]],secretAccessKey:[e?e.secretAccessKey:null,[D.required]],region:[e?e.region:null,[D.required]]})}}e("SqsConfigComponent",Hn),Hn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Hn,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Hn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Hn,selector:"tb-external-node-sqs-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:me.SafePipe,name:"safe"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Hn,decorators:[{type:n,args:[{selector:"tb-external-node-sqs-config",template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class zn{}e("RulenodeCoreConfigExternalModule",zn),zn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:zn,deps:[],target:t.ɵɵFactoryTarget.NgModule}),zn.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:zn,declarations:[_n,Hn,wn,Dn,Vn,Pn,Rn,On,Bn,En,Kn,Un],imports:[O,T,qe,vn],exports:[_n,Hn,wn,Dn,Vn,Pn,Rn,On,Bn,En,Kn,Un]}),zn.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:zn,imports:[O,T,qe,vn]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:zn,decorators:[{type:l,args:[{declarations:[_n,Hn,wn,Dn,Vn,Pn,Rn,On,Bn,En,Kn,Un],imports:[O,T,qe,vn],exports:[_n,Hn,wn,Dn,Vn,Pn,Rn,On,Bn,En,Kn,Un]}]}]});class jn extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.searchText=""}configForm(){return this.alarmStatusConfigForm}prepareInputConfig(e){return{alarmStatusList:Y(e?.alarmStatusList)?e.alarmStatusList:null}}onConfigurationSet(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e.alarmStatusList,[D.required]]})}}e("CheckAlarmStatusComponent",jn),jn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:jn,deps:[{token:G.Store},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),jn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:jn,selector:"tb-filter-node-check-alarm-status-config",usesInheritance:!0,ngImport:t,template:'
\n
\n
tb.rulenode.alarm-status
\n
\n tb.rulenode.alarm-required\n
\n
\n \n
\n\n\n\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:hn,selector:"tb-alarm-status-select"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:jn,decorators:[{type:n,args:[{selector:"tb-filter-node-check-alarm-status-config",template:'
\n
\n
tb.rulenode.alarm-status
\n
\n tb.rulenode.alarm-required\n
\n
\n \n
\n\n\n\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder}]}});class Qn extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.checkMessageConfigForm}prepareInputConfig(e){return{messageNames:Y(e?.messageNames)?e.messageNames:null,metadataNames:Y(e?.metadataNames)?e.metadataNames:null,checkAllKeys:!!Y(e?.checkAllKeys)&&e.checkAllKeys}}atLeastOne(e,t=null){return n=>{t||(t=Object.keys(n.controls));return n?.controls&&t.some((t=>!e(n.controls[t])))?null:{atLeastOne:!0}}}onConfigurationSet(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e.messageNames,[]],metadataNames:[e.metadataNames,[]],checkAllKeys:[e.checkAllKeys,[]]},{validators:this.atLeastOne(D.required,["messageNames","metadataNames"])})}touched(){this.checkMessageConfigForm.markAsTouched(),this.checkMessageConfigForm.updateValueAndValidity({emitEvent:!0})}}e("CheckMessageConfigComponent",Qn),Qn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Qn,deps:[{token:G.Store},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Qn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Qn,selector:"tb-filter-node-check-message-config",usesInheritance:!0,ngImport:t,template:'
\n
\n
tb.rulenode.fields-to-check
\n
\n tb.rulenode.at-least-one-field-required\n
\n
\n \n help\n \n \n help\n \n
\n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
\n
\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"component",type:Re.StringItemsListComponent,selector:"tb-string-items-list",inputs:["required","disabled","label","placeholder","hint","requiredText","floatLabel","appearance","editable","subscriptSizing","predefinedValues"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"component",type:Ee.MatSlideToggle,selector:"mat-slide-toggle",inputs:["disabled","disableRipple","color","tabIndex"],exportAs:["matSlideToggle"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:De.HintTooltipIconComponent,selector:"[tb-hint-tooltip-icon]",inputs:["tb-hint-tooltip-icon","tooltipPosition","hintIcon"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Qn,decorators:[{type:n,args:[{selector:"tb-filter-node-check-message-config",template:'
\n
\n
tb.rulenode.fields-to-check
\n
\n tb.rulenode.at-least-one-field-required\n
\n
\n \n help\n \n \n help\n \n
\n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
\n
\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder}]}});class $n extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.entitySearchDirection=Object.values(g),this.entitySearchDirectionTranslationsMap=y}configForm(){return this.checkRelationConfigForm}prepareInputConfig(e){return{checkForSingleEntity:!!Y(e?.checkForSingleEntity)&&e.checkForSingleEntity,direction:Y(e?.direction)?e.direction:null,entityType:Y(e?.entityType)?e.entityType:null,entityId:Y(e?.entityId)?e.entityId:null,relationType:Y(e?.relationType)?e.relationType:null}}onConfigurationSet(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[e.checkForSingleEntity,[]],direction:[e.direction,[]],entityType:[e.entityType,e&&e.checkForSingleEntity?[D.required]:[]],entityId:[e.entityId,e&&e.checkForSingleEntity?[D.required]:[]],relationType:[e.relationType,[D.required]]})}validatorTriggers(){return["checkForSingleEntity"]}updateValidators(e){const t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[D.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[D.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})}}e("CheckRelationConfigComponent",$n),$n.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:$n,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),$n.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:$n,selector:"tb-filter-node-check-relation-config",usesInheritance:!0,ngImport:t,template:'
\n
tb.rulenode.relation-search-parameters
\n
\n \n {{ \'relation.direction\' | translate }}\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n \n \n
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
\n
\n \n \n \n \n
\n
\n
\n',styles:[":host .slide-toggle{margin-bottom:18px}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:Qe.EntityAutocompleteComponent,selector:"tb-entity-autocomplete",inputs:["entityType","entitySubtype","excludeEntityIds","labelText","requiredText","useFullEntityId","appearance","required","disabled"],outputs:["entityChanged"]},{kind:"component",type:ue.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","filterAllowedEntityTypes","showLabel","required","disabled"]},{kind:"component",type:Ge.RelationTypeAutocompleteComponent,selector:"tb-relation-type-autocomplete",inputs:["showLabel","additionalClasses","appearance","required","disabled","subscriptSizing"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:Ee.MatSlideToggle,selector:"mat-slide-toggle",inputs:["disabled","disableRipple","color","tabIndex"],exportAs:["matSlideToggle"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:De.HintTooltipIconComponent,selector:"[tb-hint-tooltip-icon]",inputs:["tb-hint-tooltip-icon","tooltipPosition","hintIcon"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:$n,decorators:[{type:n,args:[{selector:"tb-filter-node-check-relation-config",template:'
\n
tb.rulenode.relation-search-parameters
\n
\n \n {{ \'relation.direction\' | translate }}\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n \n \n
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
\n
\n \n \n \n \n
\n
\n
\n',styles:[":host .slide-toggle{margin-bottom:18px}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Jn extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.perimeterType=lt,this.perimeterTypes=Object.values(lt),this.perimeterTypeTranslationMap=st,this.rangeUnits=Object.values(pt),this.rangeUnitTranslationMap=dt,this.defaultPaddingEnable=!0}configForm(){return this.geoFilterConfigForm}prepareInputConfig(e){return{latitudeKeyName:Y(e?.latitudeKeyName)?e.latitudeKeyName:null,longitudeKeyName:Y(e?.longitudeKeyName)?e.longitudeKeyName:null,perimeterType:Y(e?.perimeterType)?e.perimeterType:null,fetchPerimeterInfoFromMessageMetadata:!!Y(e?.fetchPerimeterInfoFromMessageMetadata)&&e.fetchPerimeterInfoFromMessageMetadata,perimeterKeyName:Y(e?.perimeterKeyName)?e.perimeterKeyName:null,centerLatitude:Y(e?.centerLatitude)?e.centerLatitude:null,centerLongitude:Y(e?.centerLongitude)?e.centerLongitude:null,range:Y(e?.range)?e.range:null,rangeUnit:Y(e?.rangeUnit)?e.rangeUnit:null,polygonsDefinition:Y(e?.polygonsDefinition)?e.polygonsDefinition:null}}onConfigurationSet(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e.latitudeKeyName,[D.required]],longitudeKeyName:[e.longitudeKeyName,[D.required]],perimeterType:[e.perimeterType,[D.required]],fetchPerimeterInfoFromMessageMetadata:[e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterKeyName:[e.perimeterKeyName,[]],centerLatitude:[e.centerLatitude,[]],centerLongitude:[e.centerLongitude,[]],range:[e.range,[]],rangeUnit:[e.rangeUnit,[]],polygonsDefinition:[e.polygonsDefinition,[]]})}validatorTriggers(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]}updateValidators(e){const t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,n=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterKeyName").setValidators([D.required]):this.geoFilterConfigForm.get("perimeterKeyName").setValidators([]),t||n!==lt.CIRCLE?(this.geoFilterConfigForm.get("centerLatitude").setValidators([]),this.geoFilterConfigForm.get("centerLongitude").setValidators([]),this.geoFilterConfigForm.get("range").setValidators([]),this.geoFilterConfigForm.get("rangeUnit").setValidators([]),this.defaultPaddingEnable=!0):(this.geoFilterConfigForm.get("centerLatitude").setValidators([D.required,D.min(-90),D.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([D.required,D.min(-180),D.max(180)]),this.geoFilterConfigForm.get("range").setValidators([D.required,D.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([D.required]),this.defaultPaddingEnable=!1),t||n!==lt.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([D.required]),this.geoFilterConfigForm.get("perimeterKeyName").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})}}e("GpsGeoFilterConfigComponent",Jn),Jn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Jn,deps:[{token:G.Store},{token:E.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Jn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Jn,selector:"tb-filter-node-gps-geofencing-config",usesInheritance:!0,ngImport:t,template:'
\n
\n
tb.rulenode.coordinate-field-names
\n
\n
\n \n {{ \'tb.rulenode.latitude-field-name\' | translate }}\n \n \n {{ \'tb.rulenode.latitude-field-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.longitude-field-name\' | translate }}\n \n \n {{ \'tb.rulenode.longitude-field-name-required\' | translate }}\n \n \n
\n
{{ \'tb.rulenode.coordinate-field-hint\' | translate }}
\n
\n
\n
\n
tb.rulenode.geofence-configuration
\n
\n \n {{ \'tb.rulenode.perimeter-type\' | translate }}\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.fetch-perimeter-info-from-metadata\' | translate }}\n \n
\n \n {{ \'tb.rulenode.perimeter-key-name\' | translate }}\n \n \n {{ \'tb.rulenode.perimeter-key-name-required\' | translate }}\n \n {{ \'tb.rulenode.perimeter-key-name-hint\' | translate }}\n \n
\n
\n \n {{ \'tb.rulenode.circle-center-latitude\' | translate }}\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.circle-center-longitude\' | translate }}\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n {{ \'tb.rulenode.range\' | translate }}\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.range-units\' | translate }}\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n {{ \'tb.rulenode.range-units-required\' | translate }}\n \n \n
\n
\n \n {{ \'tb.rulenode.polygon-definition\' | translate }}\n \n {{ \'tb.rulenode.polygon-definition-hint\' | translate }}\n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n',styles:[":host .slide-toggle{margin-bottom:18px}:host .tb-hint{padding:0}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"component",type:Ee.MatSlideToggle,selector:"mat-slide-toggle",inputs:["disabled","disableRipple","color","tabIndex"],exportAs:["matSlideToggle"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{kind:"directive",type:E.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:De.HintTooltipIconComponent,selector:"[tb-hint-tooltip-icon]",inputs:["tb-hint-tooltip-icon","tooltipPosition","hintIcon"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Jn,decorators:[{type:n,args:[{selector:"tb-filter-node-gps-geofencing-config",template:'
\n
\n
tb.rulenode.coordinate-field-names
\n
\n
\n \n {{ \'tb.rulenode.latitude-field-name\' | translate }}\n \n \n {{ \'tb.rulenode.latitude-field-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.longitude-field-name\' | translate }}\n \n \n {{ \'tb.rulenode.longitude-field-name-required\' | translate }}\n \n \n
\n
{{ \'tb.rulenode.coordinate-field-hint\' | translate }}
\n
\n
\n
\n
tb.rulenode.geofence-configuration
\n
\n \n {{ \'tb.rulenode.perimeter-type\' | translate }}\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.fetch-perimeter-info-from-metadata\' | translate }}\n \n
\n \n {{ \'tb.rulenode.perimeter-key-name\' | translate }}\n \n \n {{ \'tb.rulenode.perimeter-key-name-required\' | translate }}\n \n {{ \'tb.rulenode.perimeter-key-name-hint\' | translate }}\n \n
\n
\n \n {{ \'tb.rulenode.circle-center-latitude\' | translate }}\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.circle-center-longitude\' | translate }}\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n {{ \'tb.rulenode.range\' | translate }}\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.range-units\' | translate }}\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n {{ \'tb.rulenode.range-units-required\' | translate }}\n \n \n
\n
\n \n {{ \'tb.rulenode.polygon-definition\' | translate }}\n \n {{ \'tb.rulenode.polygon-definition-hint\' | translate }}\n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n',styles:[":host .slide-toggle{margin-bottom:18px}:host .tb-hint{padding:0}\n",':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:E.FormBuilder}]}});class Yn extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.messageTypeConfigForm}prepareInputConfig(e){return{messageTypes:Y(e?.messageTypes)?e.messageTypes:null}}onConfigurationSet(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e.messageTypes,[D.required]]})}}e("MessageTypeConfigComponent",Yn),Yn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Yn,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Yn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Yn,selector:"tb-filter-node-message-type-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n',dependencies:[{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:cn,selector:"tb-message-types-config",inputs:["required","label","placeholder","disabled"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Yn,decorators:[{type:n,args:[{selector:"tb-filter-node-message-type-config",template:'
\n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Wn extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.allowedEntityTypes=[x.DEVICE,x.ASSET,x.ENTITY_VIEW,x.TENANT,x.CUSTOMER,x.USER,x.DASHBOARD,x.RULE_CHAIN,x.RULE_NODE]}configForm(){return this.originatorTypeConfigForm}prepareInputConfig(e){return{originatorTypes:Y(e?.originatorTypes)?e.originatorTypes:null}}onConfigurationSet(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e.originatorTypes,[D.required]]})}}e("OriginatorTypeConfigComponent",Wn),Wn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Wn,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),Wn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Wn,selector:"tb-filter-node-originator-type-config",usesInheritance:!0,ngImport:t,template:'
\n \n help\n \n
\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n'],dependencies:[{kind:"component",type:$e.EntityTypeListComponent,selector:"tb-entity-type-list",inputs:["required","additionalClasses","appearance","label","floatLabel","disabled","subscriptSizing","allowedEntityTypes","emptyInputPlaceholder","filledInputPlaceholder","ignoreAuthorityFilter"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:U.MatSuffix,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Wn,decorators:[{type:n,args:[{selector:"tb-filter-node-originator-type-config",template:'
\n \n help\n \n
\n',styles:[':host .margin-8{margin:8px}:host .tb-error{letter-spacing:.25px;color:var(--mdc-theme-error, #f44336)}:host .tb-required:after{content:"*"}\n']}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class Xn extends s{constructor(e,t,n,o){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=o,this.tbelEnabled=$(this.store).tbelEnabled,this.scriptLanguage=d,this.changeScript=new r,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-filter-function"}configForm(){return this.scriptConfigForm}onConfigurationSet(e){this.scriptConfigForm=this.fb.group({scriptLang:[e.scriptLang,[D.required]],jsScript:[e.jsScript,[]],tbelScript:[e.tbelScript,[]]})}validatorTriggers(){return["scriptLang"]}updateValidators(e){let t=this.scriptConfigForm.get("scriptLang").value;t!==d.TBEL||this.tbelEnabled||(t=d.JS,this.scriptConfigForm.get("scriptLang").patchValue(t,{emitEvent:!1}),setTimeout((()=>{this.scriptConfigForm.updateValueAndValidity({emitEvent:!0})}))),this.scriptConfigForm.get("jsScript").setValidators(t===d.JS?[D.required]:[]),this.scriptConfigForm.get("jsScript").updateValueAndValidity({emitEvent:e}),this.scriptConfigForm.get("tbelScript").setValidators(t===d.TBEL?[D.required]:[]),this.scriptConfigForm.get("tbelScript").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=d.JS)),{scriptLang:Y(e?.scriptLang)?e.scriptLang:d.JS,jsScript:Y(e?.jsScript)?e.jsScript:null,tbelScript:Y(e?.tbelScript)?e.tbelScript:null}}testScript(e){const t=this.scriptConfigForm.get("scriptLang").value,n=t===d.JS?"jsScript":"tbelScript",r=t===d.JS?"rulenode/filter_node_script_fn":"rulenode/tbel/filter_node_script_fn",o=this.scriptConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.scriptConfigForm.get(n).setValue(e),this.changeScript.emit())}))}onValidate(){this.scriptConfigForm.get("scriptLang").value===d.JS&&this.jsFuncComponent.validateOnSubmit()}}e("ScriptConfigComponent",Xn),Xn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Xn,deps:[{token:G.Store},{token:E.UntypedFormBuilder},{token:J.NodeScriptTestService},{token:H.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Xn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Xn,selector:"tb-filter-node-script-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n \n \n \n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ee.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:te.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:oe.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Xn,decorators:[{type:n,args:[{selector:"tb-filter-node-script-config",template:'
\n \n \n \n \n \n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder},{type:J.NodeScriptTestService},{type:H.TranslateService}]},propDecorators:{jsFuncComponent:[{type:o,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:o,args:["tbelFuncComponent",{static:!1}]}]}});class Zn extends s{constructor(e,t,n,o){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=o,this.tbelEnabled=$(this.store).tbelEnabled,this.scriptLanguage=d,this.changeScript=new r,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-switch-function"}configForm(){return this.switchConfigForm}onConfigurationSet(e){this.switchConfigForm=this.fb.group({scriptLang:[e.scriptLang,[D.required]],jsScript:[e.jsScript,[]],tbelScript:[e.tbelScript,[]]})}validatorTriggers(){return["scriptLang"]}updateValidators(e){let t=this.switchConfigForm.get("scriptLang").value;t!==d.TBEL||this.tbelEnabled||(t=d.JS,this.switchConfigForm.get("scriptLang").patchValue(t,{emitEvent:!1}),setTimeout((()=>{this.switchConfigForm.updateValueAndValidity({emitEvent:!0})}))),this.switchConfigForm.get("jsScript").setValidators(t===d.JS?[D.required]:[]),this.switchConfigForm.get("jsScript").updateValueAndValidity({emitEvent:e}),this.switchConfigForm.get("tbelScript").setValidators(t===d.TBEL?[D.required]:[]),this.switchConfigForm.get("tbelScript").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=d.JS)),{scriptLang:Y(e?.scriptLang)?e.scriptLang:d.JS,jsScript:Y(e?.jsScript)?e.jsScript:null,tbelScript:Y(e?.tbelScript)?e.tbelScript:null}}testScript(e){const t=this.switchConfigForm.get("scriptLang").value,n=t===d.JS?"jsScript":"tbelScript",r=t===d.JS?"rulenode/switch_node_script_fn":"rulenode/tbel/switch_node_script_fn",o=this.switchConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.switchConfigForm.get(n).setValue(e),this.changeScript.emit())}))}onValidate(){this.switchConfigForm.get("scriptLang").value===d.JS&&this.jsFuncComponent.validateOnSubmit()}}e("SwitchConfigComponent",Zn),Zn.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Zn,deps:[{token:G.Store},{token:E.UntypedFormBuilder},{token:J.NodeScriptTestService},{token:H.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Zn.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:Zn,selector:"tb-filter-node-switch-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n \n \n \n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ee.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:te.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:oe.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:Zn,decorators:[{type:n,args:[{selector:"tb-filter-node-switch-config",template:'
\n \n \n \n \n \n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder},{type:J.NodeScriptTestService},{type:H.TranslateService}]},propDecorators:{jsFuncComponent:[{type:o,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:o,args:["tbelFuncComponent",{static:!1}]}]}});class er{}e("RuleNodeCoreConfigFilterModule",er),er.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:er,deps:[],target:t.ɵɵFactoryTarget.NgModule}),er.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:er,declarations:[Qn,$n,Jn,Yn,Wn,Xn,Zn,jn],imports:[O,T,vn],exports:[Qn,$n,Jn,Yn,Wn,Xn,Zn,jn]}),er.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:er,imports:[O,T,vn]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:er,decorators:[{type:l,args:[{declarations:[Qn,$n,Jn,Yn,Wn,Xn,Zn,jn],imports:[O,T,vn],exports:[Qn,$n,Jn,Yn,Wn,Xn,Zn,jn]}]}]});class tr extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.originatorSource=at,this.originatorSources=Object.keys(at),this.originatorSourceTranslationMap=it,this.allowedEntityTypes=[x.DEVICE,x.ASSET,x.ENTITY_VIEW,x.USER,x.EDGE]}configForm(){return this.changeOriginatorConfigForm}onConfigurationSet(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[D.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationsQuery:[e?e.relationsQuery:null,[]]})}validatorTriggers(){return["originatorSource"]}updateValidators(e){const t=this.changeOriginatorConfigForm.get("originatorSource").value;t===at.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([D.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),t===at.ENTITY?(this.changeOriginatorConfigForm.get("entityType").setValidators([D.required]),this.changeOriginatorConfigForm.get("entityNamePattern").setValidators([D.required,D.pattern(/.*\S.*/)])):(this.changeOriginatorConfigForm.get("entityType").patchValue(null,{emitEvent:e}),this.changeOriginatorConfigForm.get("entityNamePattern").patchValue(null,{emitEvent:e}),this.changeOriginatorConfigForm.get("entityType").setValidators([]),this.changeOriginatorConfigForm.get("entityNamePattern").setValidators([])),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e}),this.changeOriginatorConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.changeOriginatorConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})}}e("ChangeOriginatorConfigComponent",tr),tr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:tr,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),tr.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:tr,selector:"tb-transformation-node-change-originator-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n
\n \n \n \n
\n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ue.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","filterAllowedEntityTypes","showLabel","required","disabled"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:_.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"component",type:xn,selector:"tb-relations-query-config-old",inputs:["disabled","required"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:tr,decorators:[{type:n,args:[{selector:"tb-transformation-node-change-originator-config",template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n
\n \n \n \n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class nr extends s{constructor(e,t,n,o){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=n,this.translate=o,this.tbelEnabled=$(this.store).tbelEnabled,this.scriptLanguage=d,this.changeScript=new r,this.hasScript=!0,this.testScriptLabel="tb.rulenode.test-transformer-function"}configForm(){return this.scriptConfigForm}onConfigurationSet(e){this.scriptConfigForm=this.fb.group({scriptLang:[e?e.scriptLang:d.JS,[D.required]],jsScript:[e?e.jsScript:null,[D.required]],tbelScript:[e?e.tbelScript:null,[]]})}validatorTriggers(){return["scriptLang"]}updateValidators(e){let t=this.scriptConfigForm.get("scriptLang").value;t!==d.TBEL||this.tbelEnabled||(t=d.JS,this.scriptConfigForm.get("scriptLang").patchValue(t,{emitEvent:!1}),setTimeout((()=>{this.scriptConfigForm.updateValueAndValidity({emitEvent:!0})}))),this.scriptConfigForm.get("jsScript").setValidators(t===d.JS?[D.required]:[]),this.scriptConfigForm.get("jsScript").updateValueAndValidity({emitEvent:e}),this.scriptConfigForm.get("tbelScript").setValidators(t===d.TBEL?[D.required]:[]),this.scriptConfigForm.get("tbelScript").updateValueAndValidity({emitEvent:e})}prepareInputConfig(e){return e&&(e.scriptLang||(e.scriptLang=d.JS)),e}testScript(e){const t=this.scriptConfigForm.get("scriptLang").value,n=t===d.JS?"jsScript":"tbelScript",r=t===d.JS?"rulenode/transformation_node_script_fn":"rulenode/tbel/transformation_node_script_fn",o=this.scriptConfigForm.get(n).value;this.nodeScriptTestService.testNodeScript(o,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId,r,t,e).subscribe((e=>{e&&(this.scriptConfigForm.get(n).setValue(e),this.changeScript.emit())}))}onValidate(){this.scriptConfigForm.get("scriptLang").value===d.JS&&this.jsFuncComponent.validateOnSubmit()}}e("TransformScriptConfigComponent",nr),nr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:nr,deps:[{token:G.Store},{token:E.UntypedFormBuilder},{token:J.NodeScriptTestService},{token:H.TranslateService}],target:t.ɵɵFactoryTarget.Component}),nr.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:nr,selector:"tb-transformation-node-script-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0},{propertyName:"tbelFuncComponent",first:!0,predicate:["tbelFuncComponent"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n \n \n
\n \n
\n
\n',dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ee.JsFuncComponent,selector:"tb-js-func",inputs:["functionTitle","functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","minHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","scriptLanguage","noValidate","required"]},{kind:"component",type:te.MatButton,selector:" button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:te.MatIconButton,selector:"button[mat-icon-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:re.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:oe.TbScriptLangComponent,selector:"tb-script-lang",inputs:["disabled"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:nr,decorators:[{type:n,args:[{selector:"tb-transformation-node-script-config",template:'
\n \n \n \n \n \n \n \n
\n \n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder},{type:J.NodeScriptTestService},{type:H.TranslateService}]},propDecorators:{jsFuncComponent:[{type:o,args:["jsFuncComponent",{static:!1}]}],tbelFuncComponent:[{type:o,args:["tbelFuncComponent",{static:!1}]}]}});class rr extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.mailBodyTypes=[{name:"tb.mail-body-type.plain-text",value:"false"},{name:"tb.mail-body-type.html",value:"true"},{name:"tb.mail-body-type.dynamic",value:"dynamic"}]}configForm(){return this.toEmailConfigForm}onConfigurationSet(e){this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[D.required]],toTemplate:[e?e.toTemplate:null,[D.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[D.required]],mailBodyType:[e?e.mailBodyType:null],isHtmlTemplate:[e?e.isHtmlTemplate:null,[D.required]],bodyTemplate:[e?e.bodyTemplate:null,[D.required]]})}updateValidators(e){"dynamic"===this.toEmailConfigForm.get("mailBodyType").value?this.toEmailConfigForm.get("isHtmlTemplate").enable({emitEvent:!1}):this.toEmailConfigForm.get("isHtmlTemplate").disable({emitEvent:!1}),this.toEmailConfigForm.get("isHtmlTemplate").updateValueAndValidity({emitEvent:e})}validatorTriggers(){return["mailBodyType"]}}e("ToEmailConfigComponent",rr),rr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:rr,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),rr.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:rr,selector:"tb-transformation-node-to-email-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.mail-body-type\n \n \n {{ type.name | translate }}\n \n \n \n \n tb.rulenode.dynamic-mail-body-type\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"component",type:z.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex","hideSingleSelectionIndicator"],exportAs:["matSelect"]},{kind:"component",type:j.MatOption,selector:"mat-option",exportAs:["matOption"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:me.SafePipe,name:"safe"},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:rr,decorators:[{type:n,args:[{selector:"tb-transformation-node-to-email-config",template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.mail-body-type\n \n \n {{ type.name | translate }}\n \n \n \n \n tb.rulenode.dynamic-mail-body-type\n \n tb.rulenode.general-pattern-hint\n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n tb.rulenode.general-pattern-hint\n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class or extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[ae,ie,le]}onConfigurationSet(e){this.copyKeysConfigForm=this.fb.group({fromMetadata:[e?e.fromMetadata:null,[D.required]],keys:[e?e.keys:null,[D.required]]})}configForm(){return this.copyKeysConfigForm}removeKey(e){const t=this.copyKeysConfigForm.get("keys").value,n=t.indexOf(e);n>=0&&(t.splice(n,1),this.copyKeysConfigForm.get("keys").patchValue(t,{emitEvent:!0}))}addKey(e){const t=e.input;let n=e.value;if((n||"").trim()){n=n.trim();let e=this.copyKeysConfigForm.get("keys").value;e&&-1!==e.indexOf(n)||(e||(e=[]),e.push(n),this.copyKeysConfigForm.get("keys").patchValue(e,{emitEvent:!0}))}t&&(t.value="")}}e("CopyKeysConfigComponent",or),or.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:or,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),or.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:or,selector:"tb-transformation-node-copy-keys-config",usesInheritance:!0,ngImport:t,template:'
\n
{{\'tb.rulenode.copy-from\' | translate}}
\n \n \n {{\'tb.rulenode.data-to-metadata\' | translate}}\n \n \n {{\'tb.rulenode.metadata-to-data\' | translate}}\n \n \n \n \n \n {{key}}\n close\n \n \n \n {{ \'tb.rulenode.keys-required\' | translate }}\n tb.rulenode.use-regular-expression-hint\n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:ze.MatRadioGroup,selector:"mat-radio-group",exportAs:["matRadioGroup"]},{kind:"component",type:ze.MatRadioButton,selector:"mat-radio-button",inputs:["disableRipple","tabIndex"],exportAs:["matRadioButton"]},{kind:"component",type:se.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:se.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:se.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:se.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:or,decorators:[{type:n,args:[{selector:"tb-transformation-node-copy-keys-config",template:'
\n
{{\'tb.rulenode.copy-from\' | translate}}
\n \n \n {{\'tb.rulenode.data-to-metadata\' | translate}}\n \n \n {{\'tb.rulenode.metadata-to-data\' | translate}}\n \n \n \n \n \n {{key}}\n close\n \n \n \n {{ \'tb.rulenode.keys-required\' | translate }}\n tb.rulenode.use-regular-expression-hint\n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class ar extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.renameKeysConfigForm}onConfigurationSet(e){this.renameKeysConfigForm=this.fb.group({fromMetadata:[e?e.fromMetadata:null,[D.required]],renameKeysMapping:[e?e.renameKeysMapping:null,[D.required]]})}}e("RenameKeysConfigComponent",ar),ar.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ar,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),ar.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:ar,selector:"tb-transformation-node-rename-keys-config",usesInheritance:!0,ngImport:t,template:'
\n
{{ \'tb.rulenode.rename-keys-in\' | translate }}
\n \n \n {{\'tb.rulenode.data\' | translate}}\n \n \n {{\'tb.rulenode.metadata\' | translate}}\n \n \n \n \n
\n',dependencies:[{kind:"directive",type:ze.MatRadioGroup,selector:"mat-radio-group",exportAs:["matRadioGroup"]},{kind:"component",type:ze.MatRadioButton,selector:"mat-radio-button",inputs:["disableRipple","tabIndex"],exportAs:["matRadioButton"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"component",type:Xt,selector:"tb-kv-map-config-old",inputs:["disabled","uniqueKeyValuePairValidator","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ar,decorators:[{type:n,args:[{selector:"tb-transformation-node-rename-keys-config",template:'
\n
{{ \'tb.rulenode.rename-keys-in\' | translate }}
\n \n \n {{\'tb.rulenode.data\' | translate}}\n \n \n {{\'tb.rulenode.metadata\' | translate}}\n \n \n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class ir extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.jsonPathConfigForm}onConfigurationSet(e){this.jsonPathConfigForm=this.fb.group({jsonPath:[e?e.jsonPath:null,[D.required]]})}}e("NodeJsonPathConfigComponent",ir),ir.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ir,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),ir.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:ir,selector:"tb-transformation-node-json-path-config",usesInheritance:!0,ngImport:t,template:"
\n \n {{ 'tb.rulenode.json-path-expression' | translate }}\n \n {{ 'tb.rulenode.json-path-expression-hint' | translate }}\n {{ 'tb.rulenode.json-path-expression-required' | translate }}\n \n
\n\n",dependencies:[{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatLabel,selector:"mat-label"},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ir,decorators:[{type:n,args:[{selector:"tb-transformation-node-json-path-config",template:"
\n \n {{ 'tb.rulenode.json-path-expression' | translate }}\n \n {{ 'tb.rulenode.json-path-expression-hint' | translate }}\n {{ 'tb.rulenode.json-path-expression-required' | translate }}\n \n
\n\n"}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class lr extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[ae,ie,le]}onConfigurationSet(e){this.deleteKeysConfigForm=this.fb.group({fromMetadata:[e?e.fromMetadata:null,[D.required]],keys:[e?e.keys:null,[D.required]]})}configForm(){return this.deleteKeysConfigForm}removeKey(e){const t=this.deleteKeysConfigForm.get("keys").value,n=t.indexOf(e);n>=0&&(t.splice(n,1),this.deleteKeysConfigForm.get("keys").patchValue(t,{emitEvent:!0}))}addKey(e){const t=e.input;let n=e.value;if((n||"").trim()){n=n.trim();let e=this.deleteKeysConfigForm.get("keys").value;e&&-1!==e.indexOf(n)||(e||(e=[]),e.push(n),this.deleteKeysConfigForm.get("keys").patchValue(e,{emitEvent:!0}))}t&&(t.value="")}}e("DeleteKeysConfigComponent",lr),lr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:lr,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),lr.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:lr,selector:"tb-transformation-node-delete-keys-config",usesInheritance:!0,ngImport:t,template:'
\n
{{\'tb.rulenode.delete-from\' | translate}}
\n \n \n {{\'tb.rulenode.data\' | translate}}\n \n \n {{\'tb.rulenode.metadata\' | translate}}\n \n \n \n \n \n {{key}}\n close\n \n \n \n {{ \'tb.rulenode.keys-required\' | translate }}\n tb.rulenode.use-regular-expression-delete-hint\n \n
\n',dependencies:[{kind:"directive",type:R.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{kind:"directive",type:R.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{kind:"component",type:ne.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:K.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly"],exportAs:["matInput"]},{kind:"component",type:U.MatFormField,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:U.MatHint,selector:"mat-hint",inputs:["align","id"]},{kind:"directive",type:U.MatError,selector:"mat-error, [matError]",inputs:["id"]},{kind:"directive",type:ze.MatRadioGroup,selector:"mat-radio-group",exportAs:["matRadioGroup"]},{kind:"component",type:ze.MatRadioButton,selector:"mat-radio-button",inputs:["disableRipple","tabIndex"],exportAs:["matRadioButton"]},{kind:"component",type:se.MatChipGrid,selector:"mat-chip-grid",inputs:["tabIndex","disabled","placeholder","required","value","errorStateMatcher"],outputs:["change","valueChange"]},{kind:"directive",type:se.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputFor","matChipInputAddOnBlur","matChipInputSeparatorKeyCodes","placeholder","id","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{kind:"directive",type:se.MatChipRemove,selector:"[matChipRemove]"},{kind:"component",type:se.MatChipRow,selector:"mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]",inputs:["color","disabled","disableRipple","tabIndex","editable"],outputs:["edited"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:_.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]},{kind:"directive",type:H.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:lr,decorators:[{type:n,args:[{selector:"tb-transformation-node-delete-keys-config",template:'
\n
{{\'tb.rulenode.delete-from\' | translate}}
\n \n \n {{\'tb.rulenode.data\' | translate}}\n \n \n {{\'tb.rulenode.metadata\' | translate}}\n \n \n \n \n \n {{key}}\n close\n \n \n \n {{ \'tb.rulenode.keys-required\' | translate }}\n tb.rulenode.use-regular-expression-delete-hint\n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class sr{}e("RulenodeCoreConfigTransformModule",sr),sr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:sr,deps:[],target:t.ɵɵFactoryTarget.NgModule}),sr.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:sr,declarations:[tr,nr,rr,or,ar,ir,lr],imports:[O,T,vn],exports:[tr,nr,rr,or,ar,ir,lr]}),sr.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:sr,imports:[O,T,vn]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:sr,decorators:[{type:l,args:[{declarations:[tr,nr,rr,or,ar,ir,lr],imports:[O,T,vn],exports:[tr,nr,rr,or,ar,ir,lr]}]}]});class mr extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.entityType=x}configForm(){return this.ruleChainInputConfigForm}onConfigurationSet(e){this.ruleChainInputConfigForm=this.fb.group({ruleChainId:[e?e.ruleChainId:null,[D.required]]})}}e("RuleChainInputComponent",mr),mr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:mr,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),mr.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:mr,selector:"tb-flow-node-rule-chain-input-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n',dependencies:[{kind:"component",type:Qe.EntityAutocompleteComponent,selector:"tb-entity-autocomplete",inputs:["entityType","entitySubtype","excludeEntityIds","labelText","requiredText","useFullEntityId","appearance","required","disabled"],outputs:["entityChanged"]},{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"directive",type:E.FormControlName,selector:"[formControlName]",inputs:["formControlName","disabled","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:mr,decorators:[{type:n,args:[{selector:"tb-flow-node-rule-chain-input-config",template:'
\n \n \n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class ur extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.ruleChainOutputConfigForm}onConfigurationSet(e){this.ruleChainOutputConfigForm=this.fb.group({})}}e("RuleChainOutputComponent",ur),ur.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ur,deps:[{token:G.Store},{token:E.UntypedFormBuilder}],target:t.ɵɵFactoryTarget.Component}),ur.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"15.2.9",type:ur,selector:"tb-flow-node-rule-chain-output-config",usesInheritance:!0,ngImport:t,template:'
\n
\n
\n',dependencies:[{kind:"directive",type:_.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{kind:"directive",type:E.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:E.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"pipe",type:H.TranslatePipe,name:"translate"}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:ur,decorators:[{type:n,args:[{selector:"tb-flow-node-rule-chain-output-config",template:'
\n
\n
\n'}]}],ctorParameters:function(){return[{type:G.Store},{type:E.UntypedFormBuilder}]}});class pr{}e("RuleNodeCoreConfigFlowModule",pr),pr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:pr,deps:[],target:t.ɵɵFactoryTarget.NgModule}),pr.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:pr,declarations:[mr,ur],imports:[O,T,vn],exports:[mr,ur]}),pr.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:pr,imports:[O,T,vn]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:pr,decorators:[{type:l,args:[{declarations:[mr,ur],imports:[O,T,vn],exports:[mr,ur]}]}]});class dr{constructor(e){!function(e){e.setTranslation("en_US",{tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","output-message-type":"Output message type","output-message-type-required":"Output message type is required","output-message-type-max-length":"Output message type should be less than 256","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","interval-start":"Interval start","interval-end":"Interval end","time-unit":"Time unit","fetch-mode":"Fetch mode","order-by-timestamp":"Order by timestamp",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. If you want to fetch a single entry, select fetch mode 'First' or 'Last'.","limit-required":"Limit is required.","limit-range":"Limit should be in a range from 2 to 1000.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Allowing range from 1 to 2147483647.","start-interval-value-required":"Interval start is required.","end-interval-value-required":"Interval end is required.",filter:"Filter",switch:"Switch","math-templatization-tooltip":"This field support templatization. Use $[messageKey] to extract value from the message and ${metadataKey} to extract value from the metadata.","add-message-type":"Add message type","select-message-types-required":"At least one message type should be selected.","select-message-types":"Select message types","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one.","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","attributes-keys":"Attributes keys","attributes-keys-required":"Attributes keys are required","notify-device":"Notify device","send-attributes-updated-notification":"Send attributes updated notification","send-attributes-updated-notification-hint":"Send notification about updated attributes as a separate message to the rule engine queue.","send-attributes-deleted-notification":"Send attributes deleted notification","send-attributes-deleted-notification-hint":"Send notification about deleted attributes as a separate message to the rule engine queue.","fetch-credentials-to-metadata":"Fetch credentials to metadata","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","notify-device-delete-hint":"Send notification about deleted attributes to device.","latest-timeseries":"Latest time-series data keys","timeseries-keys":"Timeseries keys","timeseries-keys-required":"At least one timeseries key should be selected.","add-timeseries-key":"Add timeseries key","add-message-field":"Add message field","relation-search-parameters":"Relation search parameters","add-metadata-field":"Add metadata field","data-keys":"Message field names","copy-from":"Copy from","data-to-metadata":"Data to metadata","metadata-to-data":"Metadata to data","use-regular-expression-hint":"Use regular expression to copy keys by pattern",interval:"Interval","interval-required":"Interval is required","interval-hint":"Deduplication interval in seconds.","interval-min-error":"Min allowed value is 1","max-pending-msgs":"Max pending messages","max-pending-msgs-hint":"Maximum number of messages that are stored in memory for each unique deduplication id.","max-pending-msgs-required":"Max pending messages is required","max-pending-msgs-max-error":"Max allowed value is 1000","max-pending-msgs-min-error":"Min allowed value is 1","max-retries":"Max retries","max-retries-required":"Max retries is required","max-retries-hint":"Maximum number of retries to push the deduplicated messages into the queue. 10 seconds delay is used between retries","max-retries-max-error":"Max allowed value is 100","max-retries-min-error":"Min allowed value is 0",strategy:"Strategy","strategy-required":"Strategy is required","strategy-all-hint":"Return all messages that arrived during deduplication period as a single JSON array message. Where each element represents object with msg and metadata inner properties.","strategy-first-hint":"Return first message that arrived during deduplication period.","strategy-last-hint":"Return last message that arrived during deduplication period.",first:"First",last:"Last",all:"All","output-msg-type-hint":"The message type of the deduplication result.","queue-name-hint":"The queue name where the deduplication result will be published.",keys:"Keys","keys-required":"Keys are required","rename-keys-in":"Rename keys in",data:"Data",message:"Message",metadata:"Metadata","key-name":"Key name","key-name-required":"Key name is required","new-key-name":"New key name","new-key-name-required":"New key name is required","metadata-keys":"Metadata field names","json-path-expression":"JSON path expression","json-path-expression-required":"JSON path expression is required","json-path-expression-hint":"JSONPath specifies a path to an element or a set of elements in a JSON structure. '$' represents the root object or array.","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","max-relation-level-error":"Value should be greater than 0 or unspecified.","relation-type":"Relation type","relation-type-pattern":"Relation type pattern","relation-type-pattern-required":"Relation type pattern is required","relation-types-list":"Relation types to propagate","relation-types-list-hint":"If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","add-telemetry-key":"Add telemetry key","delete-from":"Delete from","use-regular-expression-delete-hint":"Use regular expression to delete keys by pattern","fetch-into":"Fetch into","attr-mapping":"Attributes mapping:","source-attribute":"Source attribute key","source-attribute-required":"Source attribute key is required.","source-telemetry":"Source telemetry key","source-telemetry-required":"Source telemetry key is required.","target-key":"Target key","target-key-required":"Target key is required.","attr-mapping-required":"At least one mapping entry should be specified.","fields-mapping":"Fields mapping*","fields-mapping-required":"At least one field mapping should be specified.","at-least-one-field-required":"At least one input field must have a value(s) provided.","originator-fields-sv-map-hint":"Target key fields support templatization. Use $[messageKey] to extract value from the message and ${metadataKey} to extract value from the metadata.","sv-map-hint":"Only target key fields support templatization. Use $[messageKey] to extract value from the message and ${metadataKey} to extract value from the metadata.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","originator-entity":"Entity","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata or data assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","select-entity-types":"Select entity types","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required","alarm-severity-pattern":"Alarm severity pattern","alarm-status-filter":"Alarm status filter","alarm-status-list-empty":"Alarm status list is empty","no-alarm-status-matching":"No alarm status matching were found.",propagate:"Propagate alarm to related entities","propagate-to-owner":"Propagate alarm to entity owner (Customer or Tenant)","propagate-to-tenant":"Propagate alarm to Tenant",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":'Comma separated address list, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","body-template":"Body Template","body-template-required":"Body Template is required","dynamic-mail-body-type":"Dynamic mail body type","mail-body-type":"Mail body type","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","ignore-request-body":"Without request body","trim-double-quotes":"Message without quotes","trim-double-quotes-hint":"If selected, request body message payload will be sent without double quotes, i.e. msg = message body","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in header/value fields',header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","key-pattern":"Key pattern","key-pattern-hint":"Optional. If a valid partition number is specified, it will be used when sending the record. If no partition is specified, the key will be used instead. If neither is specified, a partition will be assigned in a round-robin fashion.","topic-pattern-required":"Topic pattern is required",topic:"Topic","topic-required":"Topic is required","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in name/value fields',"connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","client-id-hint":'Optional. Leave empty for auto-generated Client ID. Be careful when specifying the Client ID. Majority of the MQTT brokers will not allow multiple connections with the same Client ID. To connect to such brokers, your mqtt Client ID must be unique. When platform is running in a micro-services mode, the copy of rule node is launched in each micro-service. This will automatically lead to multiple mqtt clients with the same ID and may cause failures of the rule node. To avoid such failures enable "Add Service ID as suffix to Client ID" option below.',"append-client-id-suffix":"Add Service ID as suffix to Client ID","client-id-suffix-hint":'Optional. Applied when "Client ID" specified explicitly. If selected then Service ID will be added to Client ID as a suffix. Helps to avoid failures when platform is running in a micro-services mode.',"device-id":"Device ID","device-id-required":"Device ID is required.","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","credentials-pem-hint":"At least Server CA certificate file or a pair of Client certificate and Client private key files are required","credentials-sas":"Shared Access Signature","sas-key":"SAS Key","sas-key-required":"SAS Key is required.",hostname:"Hostname","hostname-required":"Hostname is required.","azure-ca-cert":"CA certificate file","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"Server CA certificate file","private-key":"Client private key file",cert:"Client certificate file","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-dynamic-interval":"Use dynamic interval","metadata-dynamic-interval-hint":"Interval start and end input fields support templatization. Note that the substituted template value should be set in milliseconds. Use $[messageKey] to extract value from the message and ${metadataKey} to extract value from the metadata.","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata or data assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","overwrite-alarm-details":"Overwrite alarm details","use-alarm-severity-pattern":"Use alarm severity pattern","check-all-keys":"Check that all specified fields are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-to-specific-entity-tooltip":"If enabled, checks the presence of relation with a specific entity otherwise, checks the presence of relation with any entity. In both cases, relation lookup is based on configured direction and type.","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval":"Interval start","end-interval":"Interval end","start-interval-required":"Interval start is required.","end-interval-required":"Interval end is required.","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","tls-version":"TLS version","enable-proxy":"Enable proxy","use-system-proxy-properties":"Use system proxy properties","proxy-host":"Proxy host","proxy-host-required":"Proxy host is required.","proxy-port":"Proxy port","proxy-port-required":"Proxy port is required.","proxy-port-range":"Proxy port should be in a range from 1 to 65535.","proxy-user":"Proxy user","proxy-password":"Proxy password","proxy-scheme":"Proxy scheme","numbers-to-template":"Phone Numbers To Template","numbers-to-template-required":"Phone Numbers To Template is required","numbers-to-template-hint":'Comma separated Phone Numbers, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"sms-message-template":"SMS message Template","sms-message-template-required":"SMS message Template is required","use-system-sms-settings":"Use system SMS provider settings","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'Press "Enter" to complete field input.',"select-details":"Select details","entity-details-id":"Id","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-city":"City","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","fields-to-check":"Fields to check","add-detail":"Add detail","check-all-keys-tooltip":"If enabled, checks the presence of all fields listed in the message and metadata field names within the incoming message and its metadata.","fields-to-check-hint":'Press "Enter" to complete field name input. Multiple field names supported.',"entity-details-list-empty":"At least one detail should be selected.","alarm-status":"Alarm status","alarm-required":"At least one alarm status should be selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"Enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-field-name":"Latitude field name","longitude-field-name":"Longitude field name","latitude-field-name-required":"Latitude field name is required.","longitude-field-name-required":"Longitude field name is required.","fetch-perimeter-info-from-metadata":"Fetch perimeter information from metadata","fetch-perimeter-info-from-metadata-tooltip":"If perimeter type is set to 'Polygon' the value of metadata field '{{perimeterKeyName}}' will be set as perimeter definition without additional parsing of the value. Otherwise, if perimeter type is set to 'Circle' the value of '{{perimeterKeyName}}' metadata field will be parsed to extract 'latitude', 'longitude', 'radius', 'radiusUnit' fields that uses for circle perimeter definition.","perimeter-key-name":"Perimeter key name","perimeter-key-name-hint":"Metadata field name that includes perimeter information.","perimeter-key-name-required":"Perimeter key name is required.","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units","range-units-required":"Range units is required.",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch timestamp for the latest telemetry values","get-latest-value-with-ts-hint":'If selected, the latest telemetry values will also include timestamp, e.g: "temp": "{"ts":1574329385897, "value":42}"',"use-redis-queue":"Use redis queue for message persistence","ignore-null-strings":"Ignore null strings","ignore-null-strings-hint":"If selected rule node will ignore entity fields with empty value.","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size","add-metadata-key-values-as-kafka-headers":"Add Message metadata key-value pairs to Kafka record headers","add-metadata-key-values-as-kafka-headers-hint":"If selected, key-value pairs from message metadata will be added to the outgoing records headers as byte arrays with predefined charset encoding.","charset-encoding":"Charset encoding","charset-encoding-required":"Charset encoding is required.","charset-us-ascii":"US-ASCII","charset-iso-8859-1":"ISO-8859-1","charset-utf-8":"UTF-8","charset-utf-16be":"UTF-16BE","charset-utf-16le":"UTF-16LE","charset-utf-16":"UTF-16","select-queue-hint":"The queue name can be selected from a drop-down list or add a custom name.","persist-alarm-rules":"Persist state of alarm rules","fetch-alarm-rules":"Fetch state of alarm rules","input-value-key":"Input value key","input-value-key-required":"Input value key is required.","output-value-key":"Output value key","output-value-key-required":"Output value key is required.","number-of-digits-after-floating-point":"Number of digits after floating point","number-of-digits-after-floating-point-range":"Number of digits after floating point should be in a range from 0 to 15.","failure-if-delta-negative":"Tell Failure if delta is negative","failure-if-delta-negative-tooltip":"Rule node forces failure of message processing if delta value is negative.","use-caching":"Use caching","use-caching-tooltip":'Rule node will cache the value of "{{inputValueKey}}" that arrives from the incoming message to improve performance. Note that the cache will not be updated if you modify the "{{inputValueKey}}" value elsewhere.',"add-time-difference-between-readings":'Add the time difference between "{{inputValueKey}}" readings',"add-time-difference-between-readings-tooltip":'If enabled, the rule node will add the "{{periodValueKey}}" to the outbound message.',"period-value-key":"Period value key","period-value-key-required":"Period value key is required.","general-pattern-hint":"Use ${metadataKey} for value from metadata, $[messageKey] for value from message body.","alarm-severity-pattern-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body. Alarm severity should be system (CRITICAL, MAJOR etc.)',"output-node-name-hint":"The rule node name corresponds to the relation type of the output message, and it is used to forward messages to other rule nodes in the caller rule chain.","skip-latest-persistence":"Skip latest persistence","use-server-ts":"Use server ts","use-server-ts-hint":"Enable this setting to use the timestamp of the message processing instead of the timestamp from the message. Useful for all sorts of sequential processing if you merge messages from multiple sources (devices, assets, etc).","kv-map-pattern-hint":"All input fields support templatization. Use $[messageKey] to extract value from the message and ${metadataKey} to extract value from the metadata.","shared-scope":"Shared scope","server-scope":"Server scope","client-scope":"Client scope","attribute-type":"Attribute","constant-type":"Constant","time-series-type":"Time-series","message-body-type":"Message","message-metadata-type":"Metadata","argument-tile":"Arguments","no-arguments-prompt":"No arguments configured","result-title":"Result","functions-field-input":"Functions","no-option-found":"No option found","argument-source-field-input":"Source","argument-source-field-input-required":"Argument source is required.","argument-key-field-input":"Key","argument-key-field-input-required":"Argument key is required.","constant-value-field-input":"Constant value","constant-value-field-input-required":"Constant value is required.","attribute-scope-field-input":"Attribute scope","attribute-scope-field-input-required":"Attribute scope os required.","default-value-field-input":"Default value","type-field-input":"Type","type-field-input-required":"Type is required.","key-field-input":"Key","add-entity-type":"Add entity type","key-field-input-required":"Key is required.","number-floating-point-field-input":"Number of digits after floating point","number-floating-point-field-input-hint":"Use 0 to convert result to integer","add-to-message-field-input":"Add to message","add-to-metadata-field-input":"Add to metadata","custom-expression-field-input":"Mathematical Expression","custom-expression-field-input-required":"Mathematical expression is required","custom-expression-field-input-hint":"Specify a mathematical expression to evaluate. Default expression demonstrates how to transform Fahrenheit to Celsius","retained-message":"Retained","attributes-mapping":"Attributes mapping","latest-telemetry-mapping":"Latest telemetry mapping","add-mapped-attribute-to":"Add mapped attributes to","add-mapped-latest-telemetry-to":"Add mapped latest telemetry to","add-mapped-fields-to":"Add mapped fields to","add-selected-details-to":"Add selected details to","clear-selected-types":"Clear selected types","clear-selected-details":"Clear selected details","clear-selected-fields":"Clear selected fields","clear-selected-keys":"Clear selected keys","geofence-configuration":"Geofence configuration","coordinate-field-names":"Coordinate field names","coordinate-field-hint":"Rule node tries to retrieve the specified fields from the message. If they are not present, it will look them up in the metadata.","fetch-credentials-to":"Fetch credentials to","add-originator-attributes-to":"Add originator attributes to","originator-attributes":"Originator attributes","fetch-latest-telemetry-with-timestamp":"Fetch latest telemetry with timestamp","fetch-latest-telemetry-with-timestamp-tooltip":'If selected, latest telemetry values will be added to the outbound metadata with timestamp, e.g: "{{latestTsKeyName}}": "{"ts":1574329385897, "value":42}"',"tell-failure":"Tell failure if any of the attributes are missing","tell-failure-tooltip":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"created-time":"Created time","chip-help":"Press 'Enter' to complete {{inputName}} input. \nPress 'Backspace' to delete {{inputName}}. \nMultiple values supported.",detail:"detail","field-name":"field name","device-profile":"device profile","entity-type":"entity type","message-type":"message type","timeseries-key":"timeseries key",type:"Type","first-name":"First name","last-name":"Last name",label:"Label","originator-fields-mapping":"Originator fields mapping","add-mapped-originator-fields-to":"Add mapped originator fields to",fields:"Fields","skip-empty-fields":"Skip empty fields","skip-empty-fields-tooltip":"Fields with empty values will not be added to the output message/output metadata.","fetch-interval":"Fetch interval","fetch-strategy":"Fetch strategy","fetch-timeseries-from-to":"Fetch timeseries from {{startInterval}} {{startIntervalTimeUnit}} ago to {{endInterval}} {{endIntervalTimeUnit}} ago.","fetch-timeseries-from-to-invalid":'Fetch timeseries invalid ("Interval start" should be less than "Interval end").',"use-metadata-dynamic-interval-tooltip":"If selected, the rule node will use dynamic interval start and end based on the message and metadata patterns.","all-mode-hint":'If selected fetch mode "All" rule node will retrieve telemetry from the fetch interval with configurable query parameters.',"first-mode-hint":'If selected fetch mode "First" rule node will retrieve the closest telemetry to the fetch interval\'s start.',"last-mode-hint":'If selected fetch mode "Last" rule node will retrieve the closest telemetry to the fetch interval\'s end.',ascending:"Ascending",descending:"Descending",min:"Min",max:"Max",average:"Average",sum:"Sum",count:"Count",none:"None","last-level-relation-tooltip":"If selected, the rule node will search related entities only on the level set in the max relation level.","last-level-device-relation-tooltip":"If selected, the rule node will search related devices only on the level set in the max relation level.","data-to-fetch":"Data to fetch","mapping-of-customers-fields":"Mapping of customer's fields","map-fields-required":"All mapping fields are required.",attributes:"Attributes","related-device-attributes":"Related device attributes","add-selected-attributes-to":"Add selected attributes to","device-profiles":"Device profiles","mapping-of-tenant-fields":"Mapping of tenant's fields","add-attribute-key":"Add attribute key","message-template":"Message template","message-template-required":"Message template is required","use-system-slack-settings":"Use system slack settings","slack-api-token":"Slack API token","slack-api-token-required":"Slack API token is required"},"key-val":{key:"Key",value:"Value","see-examples":"See examples.","remove-entry":"Remove entry","remove-mapping-entry":"Remove mapping entry","add-mapping-entry":"Add mapping","add-entry":"Add entry","unique-key-value-pair-error":"'{{valText}}' must be different from the current '{{keyText}}'"},"mail-body-type":{"plain-text":"Plain Text",html:"HTML",dynamic:"Dynamic"}}},!0)}(e)}}e("RuleNodeCoreConfigModule",dr),dr.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:dr,deps:[{token:H.TranslateService}],target:t.ɵɵFactoryTarget.NgModule}),dr.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"15.2.9",ngImport:t,type:dr,declarations:[Je],imports:[O,T],exports:[Cn,er,Gn,zn,sr,pr,Je]}),dr.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:dr,imports:[O,T,Cn,er,Gn,zn,sr,pr]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"15.2.9",ngImport:t,type:dr,decorators:[{type:l,args:[{declarations:[Je],imports:[O,T],exports:[Cn,er,Gn,zn,sr,pr,Je]}]}],ctorParameters:function(){return[{type:H.TranslateService}]}})}}}));//# sourceMappingURL=rulenode-core-config.js.map diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java new file mode 100644 index 0000000000..ed9053e2e2 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2023 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.rule.engine; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.common.util.ListeningExecutor; + +import java.util.concurrent.Callable; + +public class TestDbCallbackExecutor implements ListeningExecutor { + + @Override + public ListenableFuture executeAsync(Callable task) { + try { + return Futures.immediateFuture(task.call()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void execute(Runnable command) { + command.run(); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java index 132383c064..33c6071769 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.action; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import org.apache.commons.lang3.NotImplementedException; import org.junit.Before; import org.junit.Test; @@ -29,15 +28,18 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; @@ -45,6 +47,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -52,7 +55,6 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import javax.script.ScriptException; import java.io.IOException; -import java.util.concurrent.Callable; import java.util.function.Consumer; import static org.junit.Assert.assertEquals; @@ -68,11 +70,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.DataConstants.IS_CLEARED_ALARM; -import static org.thingsboard.server.common.data.DataConstants.IS_EXISTING_ALARM; -import static org.thingsboard.server.common.data.DataConstants.IS_NEW_ALARM; -import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL; -import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING; @RunWith(MockitoJUnitRunner.class) public class TbAlarmNodeTest { @@ -105,28 +102,14 @@ public class TbAlarmNodeTest { @Before public void before() { - dbExecutor = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(Runnable command) { - command.run(); - } - }; + dbExecutor = new TestDbCallbackExecutor(); } @Test - public void newAlarmCanBeCreated() throws ScriptException, IOException { + public void newAlarmCanBeCreated() { initWithCreateAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); @@ -136,7 +119,7 @@ public class TbAlarmNodeTest { .endTs(ts) .tenantId(tenantId) .originator(originator) - .severity(CRITICAL) + .severity(AlarmSeverity.CRITICAL) .propagate(true) .type("SomeType") .details(null) @@ -154,16 +137,16 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Created")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -171,10 +154,10 @@ public class TbAlarmNodeTest { } @Test - public void buildDetailsThrowsException() throws ScriptException, IOException { + public void buildDetailsThrowsException() { initWithCreateAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFailedFuture(new NotImplementedException("message"))); when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); @@ -194,10 +177,10 @@ public class TbAlarmNodeTest { } @Test - public void ifAlarmClearedCreateNew() throws ScriptException, IOException { + public void ifAlarmClearedCreateNew() { initWithCreateAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); Alarm clearedAlarm = Alarm.builder().cleared(true).acknowledged(true).build(); @@ -209,7 +192,7 @@ public class TbAlarmNodeTest { .endTs(ts) .tenantId(tenantId) .originator(originator) - .severity(CRITICAL) + .severity(AlarmSeverity.CRITICAL) .propagate(true) .type("SomeType") .details(null) @@ -228,16 +211,16 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Created")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); @@ -246,13 +229,13 @@ public class TbAlarmNodeTest { } @Test - public void alarmCanBeUpdated() throws IOException { + public void alarmCanBeUpdated() { initWithCreateAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long oldEndDate = System.currentTimeMillis(); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(WARNING).endTs(oldEndDate).build(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(AlarmSeverity.WARNING).endTs(oldEndDate).build(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(activeAlarm); @@ -260,7 +243,7 @@ public class TbAlarmNodeTest { Alarm expectedAlarm = Alarm.builder() .tenantId(tenantId) .originator(originator) - .severity(CRITICAL) + .severity(AlarmSeverity.CRITICAL) .propagate(true) .type("SomeType") .details(null) @@ -279,16 +262,16 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Updated")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_EXISTING_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_EXISTING_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -297,19 +280,19 @@ public class TbAlarmNodeTest { } @Test - public void alarmCanBeCleared() throws ScriptException, IOException { + public void alarmCanBeCleared() { initWithClearAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long oldEndDate = System.currentTimeMillis(); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(WARNING).endTs(oldEndDate).build(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(AlarmSeverity.WARNING).endTs(oldEndDate).build(); Alarm expectedAlarm = Alarm.builder() .tenantId(tenantId) .originator(originator) .cleared(true) - .severity(WARNING) + .severity(AlarmSeverity.WARNING) .propagate(false) .type("SomeType") .details(null) @@ -332,16 +315,16 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Cleared")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_CLEARED_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_CLEARED_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -352,18 +335,18 @@ public class TbAlarmNodeTest { public void alarmCanBeClearedWithAlarmOriginator() throws ScriptException, IOException { initWithClearAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", alarmOriginator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, alarmOriginator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long oldEndDate = System.currentTimeMillis(); AlarmId id = new AlarmId(alarmOriginator.getId()); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(WARNING).endTs(oldEndDate).build(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(AlarmSeverity.WARNING).endTs(oldEndDate).build(); activeAlarm.setId(id); Alarm expectedAlarm = Alarm.builder() .tenantId(tenantId) .originator(originator) .cleared(true) - .severity(WARNING) + .severity(AlarmSeverity.WARNING) .propagate(false) .type("SomeType") .details(null) @@ -387,16 +370,16 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Cleared")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(alarmOriginator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_CLEARED_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_CLEARED_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -425,14 +408,14 @@ public class TbAlarmNodeTest { String rawJson = "{\"alarmSeverity\": \"WARNING\", \"passed\": 5}"; metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); Alarm expectedAlarm = Alarm.builder() .startTs(ts) .endTs(ts) .tenantId(tenantId) .originator(originator) - .severity(WARNING) + .severity(AlarmSeverity.WARNING) .propagate(true) .type("SomeType") .details(null) @@ -454,16 +437,16 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Created")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -491,14 +474,14 @@ public class TbAlarmNodeTest { node.init(ctx, nodeConfiguration); metaData.putValue("alarmSeverity", "WARNING"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); Alarm expectedAlarm = Alarm.builder() .startTs(ts) .endTs(ts) .tenantId(tenantId) .originator(originator) - .severity(WARNING) + .severity(AlarmSeverity.WARNING) .propagate(true) .type("SomeType") .details(null) @@ -520,15 +503,15 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Created")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -540,7 +523,7 @@ public class TbAlarmNodeTest { for (int i = 0; i < 10; i++) { var config = new TbCreateAlarmNodeConfiguration(); config.setPropagateToTenant(true); - config.setSeverity(CRITICAL.name()); + config.setSeverity(AlarmSeverity.CRITICAL.name()); config.setAlarmType("SomeType" + i); config.setScriptLang(ScriptLanguage.JS); config.setAlarmDetailsBuildJs("DETAILS"); @@ -557,14 +540,14 @@ public class TbAlarmNodeTest { node.init(ctx, nodeConfiguration); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); Alarm expectedAlarm = Alarm.builder() .startTs(ts) .endTs(ts) .tenantId(tenantId) .originator(originator) - .severity(CRITICAL) + .severity(AlarmSeverity.CRITICAL) .propagateToTenant(true) .type("SomeType" + i) .details(null) @@ -585,16 +568,16 @@ public class TbAlarmNodeTest { verify(ctx, atMost(10)).tellNext(any(), eq("Created")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx, atMost(10)).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -606,7 +589,7 @@ public class TbAlarmNodeTest { try { TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration(); config.setPropagate(true); - config.setSeverity(CRITICAL.name()); + config.setSeverity(AlarmSeverity.CRITICAL.name()); config.setAlarmType("SomeType"); config.setScriptLang(ScriptLanguage.JS); config.setAlarmDetailsBuildJs("DETAILS"); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java index 22df15631e..f36cc94540 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.action; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,17 +25,19 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; -import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -47,7 +48,6 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.relation.RelationService; import java.util.Collections; -import java.util.concurrent.Callable; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -58,8 +58,6 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class TbCreateRelationNodeTest { - private static final String RELATION_TYPE_CONTAINS = "Contains"; - private TbCreateRelationNode node; @Mock @@ -78,21 +76,7 @@ public class TbCreateRelationNodeTest { @Before public void before() { - dbExecutor = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(Runnable command) { - command.run(); - } - }; + dbExecutor = new TestDbCallbackExecutor(); } @Test @@ -111,15 +95,15 @@ public class TbCreateRelationNodeTest { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("name", "AssetName"); metaData.putValue("type", "AssetType"); - msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); - when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(RELATION_TYPE_CONTAINS), eq(RelationTypeGroup.COMMON))) + when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON))) .thenReturn(Futures.immediateFuture(false)); - when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, RELATION_TYPE_CONTAINS, RelationTypeGroup.COMMON)))) + when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)))) .thenReturn(Futures.immediateFuture(true)); node.onMsg(ctx, msg); - verify(ctx).tellNext(msg, TbRelationTypes.SUCCESS); + verify(ctx).tellNext(msg, TbNodeConnectionType.SUCCESS); } @Test @@ -138,19 +122,19 @@ public class TbCreateRelationNodeTest { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("name", "AssetName"); metaData.putValue("type", "AssetType"); - msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); EntityRelation relation = new EntityRelation(); - when(ctx.getRelationService().findByToAndTypeAsync(any(), eq(msg.getOriginator()), eq(RELATION_TYPE_CONTAINS), eq(RelationTypeGroup.COMMON))) + when(ctx.getRelationService().findByToAndTypeAsync(any(), eq(msg.getOriginator()), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON))) .thenReturn(Futures.immediateFuture(Collections.singletonList(relation))); when(ctx.getRelationService().deleteRelationAsync(any(), eq(relation))).thenReturn(Futures.immediateFuture(true)); - when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(RELATION_TYPE_CONTAINS), eq(RelationTypeGroup.COMMON))) + when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON))) .thenReturn(Futures.immediateFuture(false)); - when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, RELATION_TYPE_CONTAINS, RelationTypeGroup.COMMON)))) + when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)))) .thenReturn(Futures.immediateFuture(true)); node.onMsg(ctx, msg); - verify(ctx).tellNext(msg, TbRelationTypes.SUCCESS); + verify(ctx).tellNext(msg, TbNodeConnectionType.SUCCESS); } @Test @@ -169,20 +153,17 @@ public class TbCreateRelationNodeTest { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("name", "AssetName"); metaData.putValue("type", "AssetType"); - msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); - when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(RELATION_TYPE_CONTAINS), eq(RelationTypeGroup.COMMON))) + when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON))) .thenReturn(Futures.immediateFuture(false)); - when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, RELATION_TYPE_CONTAINS, RelationTypeGroup.COMMON)))) + when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)))) .thenReturn(Futures.immediateFuture(true)); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + verify(ctx).transformMsgOriginator(msgCaptor.capture(), originatorCaptor.capture()); assertEquals(assetId, originatorCaptor.getValue()); } @@ -201,9 +182,9 @@ public class TbCreateRelationNodeTest { private TbCreateRelationNodeConfiguration createRelationNodeConfig() { TbCreateRelationNodeConfiguration configuration = new TbCreateRelationNodeConfiguration(); configuration.setDirection(EntitySearchDirection.FROM.name()); - configuration.setRelationType(RELATION_TYPE_CONTAINS); + configuration.setRelationType(EntityRelation.CONTAINS_TYPE); configuration.setEntityCacheExpiration(300); - configuration.setEntityType("ASSET"); + configuration.setEntityType(EntityType.ASSET.name()); configuration.setEntityNamePattern("${name}"); configuration.setEntityTypePattern("${type}"); configuration.setCreateEntityIfNotExists(false); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbLogNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbLogNodeTest.java index f12288da3a..885002a694 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbLogNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbLogNodeTest.java @@ -24,6 +24,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -49,7 +50,7 @@ public class TbLogNodeTest { TbLogNode node = new TbLogNode(); String data = "{\"key\": \"value\"}"; TbMsgMetaData metaData = new TbMsgMetaData(Map.of("mdKey1", "mdValue1", "mdKey2", "23")); - TbMsg msg = TbMsg.newMsg("POST_TELEMETRY", TenantId.SYS_TENANT_ID, metaData, data); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, metaData, data); String logMessage = node.toLogMessage(msg); log.info(logMessage); @@ -65,7 +66,7 @@ public class TbLogNodeTest { void givenEmptyDataMsg_whenToLog_thenReturnString() { TbLogNode node = new TbLogNode(); TbMsgMetaData metaData = new TbMsgMetaData(Collections.emptyMap()); - TbMsg msg = TbMsg.newMsg("POST_TELEMETRY", TenantId.SYS_TENANT_ID, metaData, ""); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, metaData, ""); String logMessage = node.toLogMessage(msg); log.info(logMessage); @@ -81,7 +82,7 @@ public class TbLogNodeTest { void givenNullDataMsg_whenToLog_thenReturnString() { TbLogNode node = new TbLogNode(); TbMsgMetaData metaData = new TbMsgMetaData(Collections.emptyMap()); - TbMsg msg = TbMsg.newMsg("POST_TELEMETRY", TenantId.SYS_TENANT_ID, metaData, null); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, metaData, null); String logMessage = node.toLogMessage(msg); log.info(logMessage); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java index 84deafe7e4..f44326a8d5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java @@ -35,12 +35,12 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; @@ -53,6 +53,9 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class TbMsgPushToEdgeNodeTest { + private static final List MISC_EVENTS = List.of(TbMsgType.CONNECT_EVENT, TbMsgType.DISCONNECT_EVENT, + TbMsgType.ACTIVITY_EVENT, TbMsgType.INACTIVITY_EVENT); + TbMsgPushToEdgeNode node; private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); @@ -81,8 +84,8 @@ public class TbMsgPushToEdgeNodeTest { Mockito.when(ctx.getEdgeService()).thenReturn(edgeService); Mockito.when(edgeService.findRelatedEdgeIdsByEntityId(tenantId, deviceId, new PageLink(TbMsgPushToEdgeNode.DEFAULT_PAGE_SIZE))).thenReturn(new PageData<>()); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), - TbMsgDataType.JSON, "{}", null, null); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, + TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, null, null); node.onMsg(ctx, msg); @@ -102,8 +105,8 @@ public class TbMsgPushToEdgeNodeTest { PageData edgePageData = new PageData<>(List.of(edgeId), 1, 1, false); Mockito.when(edgeService.findRelatedEdgeIdsByEntityId(tenantId, userId, new PageLink(TbMsgPushToEdgeNode.DEFAULT_PAGE_SIZE))).thenReturn(edgePageData); - TbMsg msg = TbMsg.newMsg(DataConstants.ATTRIBUTES_UPDATED, userId, new TbMsgMetaData(), - TbMsgDataType.JSON, "{}", null, null); + TbMsg msg = TbMsg.newMsg(TbMsgType.ATTRIBUTES_UPDATED, userId, TbMsgMetaData.EMPTY, + TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, null, null); node.onMsg(ctx, msg); @@ -112,9 +115,7 @@ public class TbMsgPushToEdgeNodeTest { @Test public void testMiscEventsProcessedAsAttributesUpdated() { - List miscEvents = List.of(DataConstants.CONNECT_EVENT, DataConstants.DISCONNECT_EVENT, - DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT); - for (String event : miscEvents) { + for (var event : MISC_EVENTS) { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE); testEvent(event, metaData, EdgeEventActionType.ATTRIBUTES_UPDATED, "kv"); @@ -123,14 +124,12 @@ public class TbMsgPushToEdgeNodeTest { @Test public void testMiscEventsProcessedAsTimeseriesUpdated() { - List miscEvents = List.of(DataConstants.CONNECT_EVENT, DataConstants.DISCONNECT_EVENT, - DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT); - for (String event : miscEvents) { - testEvent(event, new TbMsgMetaData(), EdgeEventActionType.TIMESERIES_UPDATED, "data"); + for (var event : MISC_EVENTS) { + testEvent(event, TbMsgMetaData.EMPTY, EdgeEventActionType.TIMESERIES_UPDATED, "data"); } } - private void testEvent(String event, TbMsgMetaData metaData, EdgeEventActionType expectedType, String dataKey) { + private void testEvent(TbMsgType event, TbMsgMetaData metaData, EdgeEventActionType expectedType, String dataKey) { Mockito.when(ctx.getTenantId()).thenReturn(tenantId); Mockito.when(ctx.getEdgeService()).thenReturn(edgeService); Mockito.when(ctx.getEdgeEventService()).thenReturn(edgeEventService); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNodeTest.java index 4ddf75f29c..785be659fd 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNodeTest.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -49,34 +50,30 @@ import static org.mockito.Mockito.when; class TbAssetTypeSwitchNodeTest { - TenantId tenantId; - AssetId assetId; - AssetId assetIdDeleted; - AssetProfile assetProfile; - TbContext ctx; - TbAssetTypeSwitchNode node; - EmptyNodeConfiguration config; - TbMsgCallback callback; - RuleEngineAssetProfileCache assetProfileCache; + private AssetId assetId; + private AssetId assetIdDeleted; + private TbContext ctx; + private TbAssetTypeSwitchNode node; + private TbMsgCallback callback; @BeforeEach void setUp() throws TbNodeException { - tenantId = new TenantId(UUID.randomUUID()); + TenantId tenantId = new TenantId(UUID.randomUUID()); assetId = new AssetId(UUID.randomUUID()); assetIdDeleted = new AssetId(UUID.randomUUID()); - assetProfile = new AssetProfile(); + AssetProfile assetProfile = new AssetProfile(); assetProfile.setTenantId(tenantId); assetProfile.setName("TestAssetProfile"); //node - config = new EmptyNodeConfiguration(); + EmptyNodeConfiguration config = new EmptyNodeConfiguration(); node = new TbAssetTypeSwitchNode(); node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); //init mock ctx = mock(TbContext.class); - assetProfileCache = mock(RuleEngineAssetProfileCache.class); + RuleEngineAssetProfileCache assetProfileCache = mock(RuleEngineAssetProfileCache.class); callback = mock(TbMsgCallback.class); when(ctx.getTenantId()).thenReturn(tenantId); @@ -121,6 +118,7 @@ class TbAssetTypeSwitchNodeTest { } private TbMsg getTbMsg(EntityId entityId) { - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(), "{}", callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, callback); } + } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeTest.java new file mode 100644 index 0000000000..a146b95ee1 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeTest.java @@ -0,0 +1,165 @@ +/** + * Copyright © 2016-2023 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.rule.engine.filter; + +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; +import org.thingsboard.rule.engine.api.RuleEngineAlarmService; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class TbCheckAlarmStatusNodeTest { + + private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); + private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID()); + private static final AlarmId ALARM_ID = new AlarmId(UUID.randomUUID()); + private static final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); + + private TbCheckAlarmStatusNode node; + + private TbContext ctx; + private RuleEngineAlarmService alarmService; + + @BeforeEach + void setUp() throws TbNodeException { + var config = new TbCheckAlarmStatusNodeConfig().defaultConfiguration(); + + ctx = mock(TbContext.class); + alarmService = mock(RuleEngineAlarmService.class); + + when(ctx.getTenantId()).thenReturn(TENANT_ID); + when(ctx.getAlarmService()).thenReturn(alarmService); + when(ctx.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + + node = new TbCheckAlarmStatusNode(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenActiveAlarm_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var alarm = new Alarm(); + alarm.setId(ALARM_ID); + alarm.setOriginator(DEVICE_ID); + alarm.setType("General Alarm"); + + String msgData = JacksonUtil.toString(alarm); + TbMsg msg = getTbMsg(msgData); + + when(alarmService.findAlarmByIdAsync(TENANT_ID, ALARM_ID)).thenReturn(Futures.immediateFuture(alarm)); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenClearedAlarm_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var alarm = new Alarm(); + alarm.setId(ALARM_ID); + alarm.setOriginator(DEVICE_ID); + alarm.setType("General Alarm"); + alarm.setCleared(true); + + String msgData = JacksonUtil.toString(alarm); + TbMsg msg = getTbMsg(msgData); + + when(alarmService.findAlarmByIdAsync(TENANT_ID, ALARM_ID)).thenReturn(Futures.immediateFuture(alarm)); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenDeletedAlarm_whenOnMsg_then_Failure() throws TbNodeException { + // GIVEN + var alarm = new Alarm(); + alarm.setId(ALARM_ID); + alarm.setOriginator(DEVICE_ID); + alarm.setType("General Alarm"); + alarm.setCleared(true); + + String msgData = JacksonUtil.toString(alarm); + TbMsg msg = getTbMsg(msgData); + + when(alarmService.findAlarmByIdAsync(TENANT_ID, ALARM_ID)).thenReturn(Futures.immediateFuture(null)); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(ctx, times(1)).tellFailure(newMsgCaptor.capture(), throwableCaptor.capture()); + verify(ctx, never()).tellSuccess(any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + Throwable value = throwableCaptor.getValue(); + assertThat(value).isInstanceOf(TbNodeException.class).hasMessage("No such alarm found."); + } + + private TbMsg getTbMsg(String msgData) { + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, msgData); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeTest.java new file mode 100644 index 0000000000..e69cc55d7e --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeTest.java @@ -0,0 +1,202 @@ +/** + * Copyright © 2016-2023 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.rule.engine.filter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class TbCheckMessageNodeTest { + + private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID()); + private static final TbMsg EMPTY_POST_ATTRIBUTES_MSG = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + private TbCheckMessageNode node; + + private TbContext ctx; + + @BeforeEach + void setUp() { + ctx = mock(TbContext.class); + node = new TbCheckMessageNode(); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenDefaultConfig_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var configuration = new TbCheckMessageNodeConfiguration().defaultConfiguration(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigWithoutCheckAllKeysAndWithEmptyLists_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var configuration = new TbCheckMessageNodeConfiguration().defaultConfiguration(); + configuration.setCheckAllKeys(false); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigWithCheckAllKeys_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var configuration = new TbCheckMessageNodeConfiguration().defaultConfiguration(); + configuration.setMessageNames(List.of("temperature-0")); + configuration.setMetadataNames(List.of("deviceName", "deviceType", "ts")); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); + + TbMsg tbMsg = getTbMsg(); + + // WHEN + node.onMsg(ctx, tbMsg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(tbMsg); + } + + @Test + void givenCustomConfigWithCheckAllKeys_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var configuration = new TbCheckMessageNodeConfiguration().defaultConfiguration(); + configuration.setMessageNames(List.of("temperature-0", "temperature-1")); + configuration.setMetadataNames(List.of("deviceName", "deviceType", "ts")); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); + + TbMsg tbMsg = getTbMsg(); + + // WHEN + node.onMsg(ctx, tbMsg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(tbMsg); + } + + @Test + void givenCustomConfigWithoutCheckAllKeys_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var configuration = new TbCheckMessageNodeConfiguration().defaultConfiguration(); + configuration.setMessageNames(List.of("temperature-0", "temperature-1")); + configuration.setCheckAllKeys(false); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); + + TbMsg tbMsg = getTbMsg(); + + // WHEN + node.onMsg(ctx, tbMsg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(tbMsg); + } + + @Test + void givenCustomConfigWithoutCheckAllKeysAndEmptyMsg_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var configuration = new TbCheckMessageNodeConfiguration().defaultConfiguration(); + configuration.setMessageNames(List.of("temperature-0", "temperature-1")); + configuration.setCheckAllKeys(false); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); + + TbMsg tbMsg = getTbMsg(true); + + // WHEN + node.onMsg(ctx, tbMsg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(tbMsg); + } + + private TbMsg getTbMsg() { + return getTbMsg(false); + } + + private TbMsg getTbMsg(boolean emptyData) { + String data = emptyData ? TbMsg.EMPTY_JSON_OBJECT : "{\"temperature-0\": 25}"; + var metadata = new TbMsgMetaData(); + metadata.putValue(DataConstants.DEVICE_NAME, "Test Device"); + metadata.putValue(DataConstants.DEVICE_TYPE, DataConstants.DEFAULT_DEVICE_TYPE); + metadata.putValue("ts", String.valueOf(System.currentTimeMillis())); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, metadata, data); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java new file mode 100644 index 0000000000..bf7fa31f18 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java @@ -0,0 +1,313 @@ +/** + * Copyright © 2016-2023 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.rule.engine.filter; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.util.TbPair; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.relation.RelationService; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class TbCheckRelationNodeTest { + + private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); + private static final DeviceId ORIGINATOR_ID = new DeviceId(UUID.randomUUID()); + private static final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); + private static final TbMsg EMPTY_POST_ATTRIBUTES_MSG = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, ORIGINATOR_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + private TbCheckRelationNode node; + + private TbContext ctx; + private RelationService relationService; + + @BeforeEach + void setUp() { + ctx = mock(TbContext.class); + relationService = mock(RelationService.class); + + when(ctx.getTenantId()).thenReturn(TENANT_ID); + when(ctx.getRelationService()).thenReturn(relationService); + when(ctx.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + + node = new TbCheckRelationNode(); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenDefaultConfig_whenInit_then_throwException() { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + + // WHEN + var exception = assertThrows(TbNodeException.class, () -> node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))); + + // THEN + assertThat(exception.getMessage()).isEqualTo("Entity should be specified!"); + } + + @Test + void givenCustomConfigWithCheckRelationToSpecificEntity_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + + AssetId assetId = new AssetId(UUID.randomUUID()); + config.setEntityType(assetId.getEntityType().name()); + config.setEntityId(assetId.getId().toString()); + + when(relationService.checkRelationAsync(TENANT_ID, ORIGINATOR_ID, assetId, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(true)); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigWithCheckRelationToSpecificEntity_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + + AssetId assetId = new AssetId(UUID.randomUUID()); + config.setEntityType(assetId.getEntityType().name()); + config.setEntityId(assetId.getId().toString()); + + when(relationService.checkRelationAsync(TENANT_ID, ORIGINATOR_ID, assetId, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(false)); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigWithCheckRelationToSpecificEntityAndDirectionTo_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + + AssetId assetId = new AssetId(UUID.randomUUID()); + config.setEntityType(assetId.getEntityType().name()); + config.setEntityId(assetId.getId().toString()); + config.setDirection(EntitySearchDirection.TO.name()); + + when(relationService.checkRelationAsync(TENANT_ID, assetId, ORIGINATOR_ID, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(true)); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigWithCheckRelationToSpecificEntityAndDirectionTo_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + + AssetId assetId = new AssetId(UUID.randomUUID()); + config.setEntityType(assetId.getEntityType().name()); + config.setEntityId(assetId.getId().toString()); + config.setDirection(EntitySearchDirection.TO.name()); + + when(relationService.checkRelationAsync(TENANT_ID, assetId, ORIGINATOR_ID, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(false)); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfig_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + config.setCheckForSingleEntity(false); + var entityRelation = new EntityRelation(); + entityRelation.setFrom(ORIGINATOR_ID); + entityRelation.setTo(new AssetId(UUID.randomUUID())); + entityRelation.setType(EntityRelation.CONTAINS_TYPE); + entityRelation.setTypeGroup(RelationTypeGroup.COMMON); + + when(relationService.findByFromAndTypeAsync(TENANT_ID, ORIGINATOR_ID, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(List.of(entityRelation))); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + verify(relationService, never()).findByToAndTypeAsync(any(), any(), anyString(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfig_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + config.setCheckForSingleEntity(false); + + when(relationService.findByFromAndTypeAsync(TENANT_ID, ORIGINATOR_ID, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(Collections.emptyList())); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + verify(relationService, never()).findByToAndTypeAsync(any(), any(), anyString(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigDirectionTo_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + config.setCheckForSingleEntity(false); + config.setDirection(EntitySearchDirection.TO.name()); + var entityRelation = new EntityRelation(); + entityRelation.setFrom(new AssetId(UUID.randomUUID())); + entityRelation.setTo(ORIGINATOR_ID); + entityRelation.setType(EntityRelation.CONTAINS_TYPE); + entityRelation.setTypeGroup(RelationTypeGroup.COMMON); + + when(relationService.findByToAndTypeAsync(TENANT_ID, ORIGINATOR_ID, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(List.of(entityRelation))); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + verify(relationService, never()).findByFromAndTypeAsync(any(), any(), anyString(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigDirectionTo_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + config.setCheckForSingleEntity(false); + config.setDirection(EntitySearchDirection.TO.name()); + + when(relationService.findByToAndTypeAsync(TENANT_ID, ORIGINATOR_ID, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(Collections.emptyList())); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + verify(relationService, never()).findByFromAndTypeAsync(any(), any(), anyString(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + config.setEntityType(ORIGINATOR_ID.getEntityType().name()); + config.setEntityId(ORIGINATOR_ID.getId().toString()); + String oldConfig = "{\"checkForSingleEntity\":true,\"direction\":\"TO\",\"entityType\":\"" + config.getEntityType() + "\",\"entityId\":\"" + config.getEntityId() + "\",\"relationType\":\"Contains\"}"; + JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); + // WHEN + TbPair upgrade = node.upgrade(0, configJson); + // THEN + assertTrue(upgrade.getFirst()); + assertEquals(config, JacksonUtil.treeToValue(upgrade.getSecond(), config.getClass())); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNodeTest.java index e7b3ccd7f1..a6f4a6e3cf 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNodeTest.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -49,34 +50,30 @@ import static org.mockito.Mockito.when; class TbDeviceTypeSwitchNodeTest { - TenantId tenantId; - DeviceId deviceId; - DeviceId deviceIdDeleted; - DeviceProfile deviceProfile; - TbContext ctx; - TbDeviceTypeSwitchNode node; - EmptyNodeConfiguration config; - TbMsgCallback callback; - RuleEngineDeviceProfileCache deviceProfileCache; + private DeviceId deviceId; + private DeviceId deviceIdDeleted; + private TbContext ctx; + private TbDeviceTypeSwitchNode node; + private TbMsgCallback callback; @BeforeEach void setUp() throws TbNodeException { - tenantId = new TenantId(UUID.randomUUID()); + TenantId tenantId = new TenantId(UUID.randomUUID()); deviceId = new DeviceId(UUID.randomUUID()); deviceIdDeleted = new DeviceId(UUID.randomUUID()); - deviceProfile = new DeviceProfile(); + DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setTenantId(tenantId); deviceProfile.setName("TestDeviceProfile"); //node - config = new EmptyNodeConfiguration(); + EmptyNodeConfiguration config = new EmptyNodeConfiguration(); node = new TbDeviceTypeSwitchNode(); node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); //init mock ctx = mock(TbContext.class); - deviceProfileCache = mock(RuleEngineDeviceProfileCache.class); + RuleEngineDeviceProfileCache deviceProfileCache = mock(RuleEngineDeviceProfileCache.class); callback = mock(TbMsgCallback.class); when(ctx.getTenantId()).thenReturn(tenantId); @@ -121,6 +118,6 @@ class TbDeviceTypeSwitchNodeTest { } private TbMsg getTbMsg(EntityId entityId) { - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(), "{}", callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, callback); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java index 4cfe38f3fc..639c1a7b1a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java @@ -29,6 +29,8 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -51,25 +53,25 @@ public class TbJsFilterNodeTest { @Mock private ScriptEngine scriptEngine; - private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); + private final RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private final RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); @Test - public void falseEvaluationDoNotSendMsg() throws TbNodeException, ScriptException { + public void falseEvaluationDoNotSendMsg() throws TbNodeException { initWithScript(); - TbMsg msg = TbMsg.newMsg("USER", null, new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFuture(false)); node.onMsg(ctx, msg); verify(ctx).getDbCallbackExecutor(); - verify(ctx).tellNext(msg, "False"); + verify(ctx).tellNext(msg, TbNodeConnectionType.FALSE); } @Test public void exceptionInJsThrowsException() throws TbNodeException { initWithScript(); TbMsgMetaData metaData = new TbMsgMetaData(); - TbMsg msg = TbMsg.newMsg("USER", null, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFailedFuture(new ScriptException("error"))); @@ -81,12 +83,12 @@ public class TbJsFilterNodeTest { public void metadataConditionCanBeTrue() throws TbNodeException { initWithScript(); TbMsgMetaData metaData = new TbMsgMetaData(); - TbMsg msg = TbMsg.newMsg("USER", null, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFuture(true)); node.onMsg(ctx, msg); verify(ctx).getDbCallbackExecutor(); - verify(ctx).tellNext(msg, "True"); + verify(ctx).tellNext(msg, TbNodeConnectionType.TRUE); } private void initWithScript() throws TbNodeException { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java index f4f852c6bd..3343c5fb92 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java @@ -29,6 +29,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -47,8 +48,8 @@ public class TbJsSwitchNodeTest { @Mock private ScriptEngine scriptEngine; - private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); + private final RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private final RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); @Test public void multipleRoutesAreAllowed() throws TbNodeException { @@ -58,7 +59,7 @@ public class TbJsSwitchNodeTest { metaData.putValue("humidity", "99"); String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; - TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); when(scriptEngine.executeSwitchAsync(msg)).thenReturn(Futures.immediateFuture(Sets.newHashSet("one", "three"))); node.onMsg(ctx, msg); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeTest.java new file mode 100644 index 0000000000..4e2e5dc430 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeTest.java @@ -0,0 +1,103 @@ +/** + * Copyright © 2016-2023 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.rule.engine.filter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; + +class TbMsgTypeFilterNodeTest { + + private DeviceId deviceId; + private TbContext ctx; + private TbMsgTypeFilterNode node; + + @BeforeEach + void setUp() throws TbNodeException { + ctx = mock(TbContext.class); + var config = new TbMsgTypeFilterNodeConfiguration().defaultConfiguration(); + deviceId = new DeviceId(UUID.randomUUID()); + node = new TbMsgTypeFilterNode(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenPostAttributes_whenOnMsg_then_True() { + // GIVEN + TbMsg msg = getTbMsg(deviceId, POST_ATTRIBUTES_REQUEST); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenAttributesUpdated_whenOnMsg_then_False() { + // GIVEN + TbMsg msg = getTbMsg(deviceId, ATTRIBUTES_UPDATED); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + private TbMsg getTbMsg(EntityId entityId, TbMsgType msgType) { + return TbMsg.newMsg(msgType, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java new file mode 100644 index 0000000000..603c23cd05 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java @@ -0,0 +1,95 @@ +/** + * Copyright © 2016-2023 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.rule.engine.filter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class TbMsgTypeSwitchNodeTest { + + private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID()); + + private TbMsgTypeSwitchNode node; + + private TbContext ctx; + + @BeforeEach + void setUp() { + ctx = mock(TbContext.class); + node = new TbMsgTypeSwitchNode(); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenAllTypes_whenOnMsg_then_allTypesSupported() throws TbNodeException { + // GIVEN + List tbMsgList = new ArrayList<>(); + var tbMsgTypes = TbMsgType.values(); + for (var msgType : tbMsgTypes) { + tbMsgList.add(getTbMsg(msgType)); + } + + // WHEN + for (TbMsg tbMsg : tbMsgList) { + node.onMsg(ctx, tbMsg); + } + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor nodeConnectionCapture = ArgumentCaptor.forClass(String.class); + verify(ctx, times(tbMsgList.size())).tellNext(newMsgCaptor.capture(), nodeConnectionCapture.capture()); + verify(ctx, never()).tellFailure(any(), any()); + var resultMsgs = newMsgCaptor.getAllValues(); + var resultNodeConnections = nodeConnectionCapture.getAllValues(); + for (int i = 0; i < resultMsgs.size(); i++) { + var msg = resultMsgs.get(i); + assertThat(msg).isNotNull(); + assertThat(msg.getType()).isNotNull(); + assertThat(msg.getType()).isEqualTo(msg.getInternalType().name()); + assertThat(msg).isSameAs(tbMsgList.get(i)); + assertThat(resultNodeConnections.get(i)) + .isEqualTo(msg.getInternalType().getRuleNodeConnection()); + } + } + + private TbMsg getTbMsg(TbMsgType msgType) { + return TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNodeTest.java new file mode 100644 index 0000000000..3ed566b7e6 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNodeTest.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2016-2023 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.rule.engine.filter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class TbOriginatorTypeFilterNodeTest { + + private TbContext ctx; + private TbOriginatorTypeFilterNode node; + + @BeforeEach + void setUp() throws TbNodeException { + ctx = mock(TbContext.class); + var config = new TbOriginatorTypeFilterNodeConfiguration().defaultConfiguration(); + node = new TbOriginatorTypeFilterNode(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenDevice_whenOnMsg_then_True() { + // GIVEN + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenAsset_whenOnMsg_then_False() { + // GIVEN + AssetId assetId = new AssetId(UUID.randomUUID()); + TbMsg msg = getTbMsg(assetId); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + private TbMsg getTbMsg(EntityId entityId) { + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNodeTest.java new file mode 100644 index 0000000000..796a7106fc --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNodeTest.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2023 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.rule.engine.filter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class TbOriginatorTypeSwitchNodeTest { + + private static final UUID RANDOM_UUID = UUID.randomUUID(); + + private TbOriginatorTypeSwitchNode node; + + private TbContext ctx; + + @BeforeEach + void setUp() { + ctx = mock(TbContext.class); + node = new TbOriginatorTypeSwitchNode(); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenAllTypes_whenOnMsg_then_allTypesSupported() throws TbNodeException { + // GIVEN + List tbMsgList = new ArrayList<>(); + var entityTypes = EntityType.values(); + for (var entityType : entityTypes) { + var entityId = EntityIdFactory.getByTypeAndUuid(entityType, RANDOM_UUID); + tbMsgList.add(getTbMsg(entityId)); + } + + // WHEN + for (TbMsg tbMsg : tbMsgList) { + node.onMsg(ctx, tbMsg); + } + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor nodeConnectionCapture = ArgumentCaptor.forClass(String.class); + verify(ctx, times(tbMsgList.size())).tellNext(newMsgCaptor.capture(), nodeConnectionCapture.capture()); + verify(ctx, never()).tellFailure(any(), any()); + var resultMsgs = newMsgCaptor.getAllValues(); + var resultNodeConnections = nodeConnectionCapture.getAllValues(); + for (int i = 0; i < resultMsgs.size(); i++) { + var msg = resultMsgs.get(i); + assertThat(msg).isNotNull(); + assertThat(msg).isSameAs(tbMsgList.get(i)); + assertThat(resultNodeConnections.get(i)) + .isEqualTo(msg.getOriginator().getEntityType().getNormalName()); + } + } + + private TbMsg getTbMsg(EntityId entityId) { + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGeoUtilTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GeoUtilTest.java similarity index 99% rename from rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGeoUtilTest.java rename to rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GeoUtilTest.java index 6a03a8f362..f53a876954 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGeoUtilTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GeoUtilTest.java @@ -21,7 +21,7 @@ import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) -public class TbGeoUtilTest { +public class GeoUtilTest { public static final String SIMPLE_RECT = "[[51.903762928405555,23.642220786948297],[44.669801219635644,41.83345155830211]]"; public static final String SIMPLE_RECT_WITH_HOLE_IN_CENTER = "[[[44.66980121963565,23.642220786948297],[44.66980121963565,41.83345155830211],[51.903762928405555,41.83345155830211],[51.903762928405555,23.642220786948297]],[[46.10464044504632,26.234282119122227],[50.8755868028522,26.25625220459488],[51.04164771375101,38.5595000692786],[45.99790855491869,38.75723083853248]]]"; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNodeTest.java new file mode 100644 index 0000000000..f4585d0f96 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNodeTest.java @@ -0,0 +1,458 @@ +/** + * Copyright © 2016-2023 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.rule.engine.geo; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class TbGpsGeofencingFilterNodeTest { + + private static final double CIRCLE_RANGE = 1.0; + private static final Coordinates CIRCLE_CENTER = new Coordinates(49.0384, 31.4513); + private static final Coordinates POINT_INSIDE_CIRCLE = new Coordinates(49.0354, 31.4513); // distance from center: 0.334 km + private static final Coordinates POINT_OUTSIDE_CIRCLE = new Coordinates(49.0284, 31.4513); // distance from center: 1.112 km + + private TbContext ctx; + private TbGpsGeofencingFilterNode node; + + @BeforeEach + void setUp() { + ctx = mock(TbContext.class); + node = new TbGpsGeofencingFilterNode(); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + // Exception tests + + @Test + void givenDefaultConfig_whenOnMsg_thenExceptionInvalidMsg() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getEmptyArrayTbMsg(deviceId); + + // WHEN + var exception = assertThrows(TbNodeException.class, () -> node.onMsg(ctx, msg)); + + // THEN + assertThat(exception.getMessage()).isEqualTo("Incoming Message is not a valid JSON object!"); + } + + @Test + void givenDefaultConfig_whenOnMsg_thenExceptionMissingPerimeterDefinitionNewVersion() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId, TbMsgMetaData.EMPTY, + GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(), GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude()); + + // WHEN + var exception = assertThrows(TbNodeException.class, () -> node.onMsg(ctx, msg)); + + // THEN + assertThat(exception.getMessage()).isEqualTo("Missing perimeter definition!"); + } + + @Test + void givenTypePolygonAndConfigWithoutPerimeterKeyName_whenOnMsg_thenExceptionMissingPerimeterDefinitionOldVersion() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterKeyName(null); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId, TbMsgMetaData.EMPTY, + GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(), GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude()); + + // WHEN + var exception = assertThrows(TbNodeException.class, () -> node.onMsg(ctx, msg)); + + // THEN + assertThat(exception.getMessage()).isEqualTo("Missing perimeter definition!"); + } + + // Polygon tests + + @Test + void givenTypePolygonAndConfigWithoutPerimeterKeyName_whenOnMsg_thenTrue() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterKeyName(null); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForOldVersionPolygonPerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(), GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypePolygonAndConfigWithoutPerimeterKeyName_whenOnMsg_thenFalse() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterKeyName(null); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForOldVersionPolygonPerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLatitude(), GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenDefaultConfig_whenOnMsg_thenTrue() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForNewVersionPolygonPerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(), GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenDefaultConfig_whenOnMsg_thenFalse() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForNewVersionPolygonPerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLatitude(), GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypePolygonAndConfigWithPolygonDefined_whenOnMsg_thenTrue() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setFetchPerimeterInfoFromMessageMetadata(false); + config.setPolygonsDefinition(GeoUtilTest.SIMPLE_RECT); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId, TbMsgMetaData.EMPTY, + GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(), GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypePolygonAndConfigWithPolygonDefined_whenOnMsg_thenFalse() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setFetchPerimeterInfoFromMessageMetadata(false); + config.setPolygonsDefinition(GeoUtilTest.SIMPLE_RECT); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId, TbMsgMetaData.EMPTY, + GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLatitude(), GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + private TbMsgMetaData getMetadataForOldVersionPolygonPerimeter() { + var metadata = new TbMsgMetaData(); + metadata.putValue("perimeter", GeoUtilTest.SIMPLE_RECT); + return metadata; + } + + private TbMsgMetaData getMetadataForNewVersionPolygonPerimeter() { + var metadata = new TbMsgMetaData(); + metadata.putValue("ss_perimeter", GeoUtilTest.SIMPLE_RECT); + return metadata; + } + + // Circle tests + + @Test + void givenTypeCircleAndConfigWithoutPerimeterKeyName_whenOnMsg_thenTrue() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterKeyName(null); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForOldVersionCirclePerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + POINT_INSIDE_CIRCLE.getLatitude(), POINT_INSIDE_CIRCLE.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypeCircleAndConfigWithoutPerimeterKeyName_whenOnMsg_thenFalse() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterKeyName(null); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForOldVersionCirclePerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + POINT_OUTSIDE_CIRCLE.getLatitude(), POINT_OUTSIDE_CIRCLE.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypeCircle_whenOnMsg_thenTrue() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterType(PerimeterType.CIRCLE); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForNewVersionCirclePerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + POINT_INSIDE_CIRCLE.getLatitude(), POINT_INSIDE_CIRCLE.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypeCircle_whenOnMsg_thenFalse() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterType(PerimeterType.CIRCLE); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForNewVersionCirclePerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + POINT_OUTSIDE_CIRCLE.getLatitude(), POINT_OUTSIDE_CIRCLE.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypeCircleAndConfigWithCircleDefined_whenOnMsg_thenTrue() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setFetchPerimeterInfoFromMessageMetadata(false); + config.setPerimeterType(PerimeterType.CIRCLE); + config.setCenterLatitude(CIRCLE_CENTER.getLatitude()); + config.setCenterLongitude(CIRCLE_CENTER.getLongitude()); + config.setRange(CIRCLE_RANGE); + config.setRangeUnit(RangeUnit.KILOMETER); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId, TbMsgMetaData.EMPTY, + POINT_INSIDE_CIRCLE.getLatitude(), POINT_INSIDE_CIRCLE.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypeCircleAndConfigWithCircleDefined_whenOnMsg_thenFalse() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setFetchPerimeterInfoFromMessageMetadata(false); + config.setPerimeterType(PerimeterType.CIRCLE); + config.setCenterLatitude(CIRCLE_CENTER.getLatitude()); + config.setCenterLongitude(CIRCLE_CENTER.getLongitude()); + config.setRange(CIRCLE_RANGE); + config.setRangeUnit(RangeUnit.KILOMETER); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId, TbMsgMetaData.EMPTY, + POINT_OUTSIDE_CIRCLE.getLatitude(), POINT_OUTSIDE_CIRCLE.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + private TbMsgMetaData getMetadataForOldVersionCirclePerimeter() { + var metadata = new TbMsgMetaData(); + metadata.putValue("centerLatitude", String.valueOf(CIRCLE_CENTER.getLatitude())); + metadata.putValue("centerLongitude", String.valueOf(CIRCLE_CENTER.getLongitude())); + metadata.putValue("range", String.valueOf(CIRCLE_RANGE)); + metadata.putValue("rangeUnit", String.valueOf(RangeUnit.KILOMETER)); + return metadata; + } + + private TbMsgMetaData getMetadataForNewVersionCirclePerimeter() { + ObjectNode perimeter = JacksonUtil.newObjectNode(); + perimeter.put("latitude", CIRCLE_CENTER.getLatitude()); + perimeter.put("longitude", CIRCLE_CENTER.getLongitude()); + perimeter.put("radius", CIRCLE_RANGE); + perimeter.put("radiusUnit", String.valueOf(RangeUnit.KILOMETER)); + var metadata = new TbMsgMetaData(); + metadata.putValue("ss_perimeter", JacksonUtil.toString(perimeter)); + return metadata; + } + + private TbMsg getTbMsg(EntityId entityId, TbMsgMetaData metadata, double latitude, double longitude) { + String data = "{\"latitude\": " + latitude + ", \"longitude\": " + longitude + "}"; + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, metadata, data); + } + + private TbMsg getEmptyArrayTbMsg(EntityId entityId) { + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java index c95183717f..bf4d090c1c 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -56,26 +57,26 @@ public class TbMsgToEmailNodeTest { private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); @Test - public void msgCanBeConverted() throws IOException { + public void msgCanBeConverted() { initWithScript(); metaData.putValue("username", "oreo"); metaData.putValue("userEmail", "user@email.io"); metaData.putValue("name", "temp"); metaData.putValue("passed", "5"); metaData.putValue("count", "100"); - TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); emailNode.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("SEND_EMAIL", typeCaptor.getValue()); + assertEquals(TbMsgType.SEND_EMAIL, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("oreo", metadataCaptor.getValue().getValue("username")); assertNotSame(metaData, metadataCaptor.getValue()); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java index 2efc438f8f..292292b89b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java @@ -15,21 +15,19 @@ */ package org.thingsboard.rule.engine.math; -import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import lombok.extern.slf4j.Slf4j; -import org.awaitility.Awaitility; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.AbstractListeningExecutor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; @@ -45,6 +43,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.attributes.AttributesService; @@ -53,26 +52,44 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Semaphore; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.lenient; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @Slf4j -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class TbMathNodeTest { - private EntityId originator = new DeviceId(Uuids.timeBased()); - private TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); + static final int RULE_DISPATCHER_POOL_SIZE = 2; + static final int DB_CALLBACK_POOL_SIZE = 3; + static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5); + private final EntityId originator = DeviceId.fromString("ccd71696-0586-422d-940e-755a41ec3b0d"); + private final TenantId tenantId = TenantId.fromUUID(UUID.fromString("e7f46b23-0c7d-42f5-9b06-fc35ab17af8a")); - @Mock + @Mock(lenient = true) private TbContext ctx; @Mock private AttributesService attributesService; @@ -80,35 +97,27 @@ public class TbMathNodeTest { private TimeseriesService tsService; @Mock private RuleEngineTelemetryService telemetryService; - private AbstractListeningExecutor dbExecutor; + private AbstractListeningExecutor dbCallbackExecutor; + private AbstractListeningExecutor ruleEngineDispatcherExecutor; - @Before + @BeforeEach public void before() { - dbExecutor = new AbstractListeningExecutor() { - @Override - protected int getThreadPollSize() { - return 3; - } - }; - dbExecutor.init(); - initMocks(); + dbCallbackExecutor = new DBCallbackExecutor(); + dbCallbackExecutor.init(); + ruleEngineDispatcherExecutor = new RuleDispatcherExecutor(); + ruleEngineDispatcherExecutor.init(); + + willReturn(dbCallbackExecutor).given(ctx).getDbCallbackExecutor(); + willReturn(attributesService).given(ctx).getAttributesService(); + willReturn(telemetryService).given(ctx).getTelemetryService(); + willReturn(tsService).given(ctx).getTimeseriesService(); + willReturn(tenantId).given(ctx).getTenantId(); } - @After + @AfterEach public void after() { - dbExecutor.destroy(); - } - - private void initMocks() { - Mockito.reset(ctx); - Mockito.reset(attributesService); - Mockito.reset(tsService); - Mockito.reset(telemetryService); - lenient().when(ctx.getAttributesService()).thenReturn(attributesService); - lenient().when(ctx.getTelemetryService()).thenReturn(telemetryService); - lenient().when(ctx.getTimeseriesService()).thenReturn(tsService); - lenient().when(ctx.getTenantId()).thenReturn(tenantId); - lenient().when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); + ruleEngineDispatcherExecutor.executor().shutdownNow(); + dbCallbackExecutor.executor().shutdownNow(); } private TbMathNode initNode(TbRuleNodeMathFunctionType operation, TbMathResult result, TbMathArgument... arguments) { @@ -149,139 +158,137 @@ public class TbMathNodeTest { metaData.putValue("key2", "argumentA"); ObjectNode msgNode = JacksonUtil.newObjectNode() .put("key3", "argumentB").put("argumentA", 2).put("argumentB", 2); - TbMsg msg = TbMsg.newMsg("TEST", originator, metaData, msgNode.toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, msgNode.toString()); node.onMsg(ctx, msg); - ConcurrentMap semaphores = (ConcurrentMap) ReflectionTestUtils.getField(node, "semaphores"); - Assert.assertNotNull(semaphores); - Semaphore originatorSemaphore = semaphores.get(originator); - Assert.assertNotNull(originatorSemaphore); - metaData.putValue("key1", "secondMsgResult"); metaData.putValue("key2", "argumentC"); msgNode = JacksonUtil.newObjectNode() .put("key3", "argumentD").put("argumentC", 4).put("argumentD", 3); - msg = TbMsg.newMsg("TEST", originator, metaData, msgNode.toString()); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, msgNode.toString()); node.onMsg(ctx, msg); - Awaitility.await("Semaphore released").atMost(5, TimeUnit.SECONDS).until(semaphores.get(originator)::tryAcquire); - ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - Mockito.verify(ctx, Mockito.times(2)).tellSuccess(msgCaptor.capture()); + verify(ctx, timeout(TIMEOUT).times(2)).tellSuccess(msgCaptor.capture()); List resultMsgs = msgCaptor.getAllValues(); - Assert.assertFalse(resultMsgs.isEmpty()); - Assert.assertEquals(2, resultMsgs.size()); + assertFalse(resultMsgs.isEmpty()); + assertEquals(2, resultMsgs.size()); for (int i = 0; i < resultMsgs.size(); i++) { TbMsg outMsg = resultMsgs.get(i); - Assert.assertNotNull(outMsg); - Assert.assertNotNull(outMsg.getData()); + assertNotNull(outMsg); + assertNotNull(outMsg.getData()); var resultJson = JacksonUtil.toJsonNode(outMsg.getData()); String resultKey = i == 0 ? "firstMsgResult" : "secondMsgResult"; - Assert.assertTrue(resultJson.has(resultKey)); - Assert.assertEquals(i == 0 ? 10 : 17, resultJson.get(resultKey).asInt()); + assertTrue(resultJson.has(resultKey)); + assertEquals(i == 0 ? 10 : 17, resultJson.get(resultKey).asInt()); } - semaphores.remove(originator); } - @Test - public void testSimpleFunctions() { - testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.ADD, 2.1, 2.2, 4.3); - testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.SUB, 2.1, 2.2, -0.1); - testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.MULT, 2.1, 2.0, 4.2); - testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.DIV, 4.2, 2.0, 2.1); - - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SIN, Math.toRadians(30), 0.5); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SIN, Math.toRadians(90), 1.0); - - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SINH, Math.toRadians(0), 0.0); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.COSH, Math.toRadians(0), 1.0); - - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.COS, Math.toRadians(60), 0.5); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.COS, Math.toRadians(0), 1.0); - - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.TAN, Math.toRadians(45), 1); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.TAN, Math.toRadians(0), 0); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.TANH, 90, 1); - - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.ACOS, 0.5, 1.05); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.ASIN, 0.5, 0.52); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.ATAN, 0.5, 0.46); - testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.ATAN2, 0.5, 0.3, 1.03); - - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.EXP, 1, 2.72); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.EXPM1, 1, 1.72); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.ABS, -1, 1); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SQRT, 4, 2); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.CBRT, 8, 2); - - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.GET_EXP, 4, 2); - testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.HYPOT, 4, 5, 6.4); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.LOG, 4, 1.39); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.LOG10, 4, 0.6); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.LOG1P, 4, 1.61); - - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.CEIL, 1.55, 2); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.FLOOR, 23.97, 23); - testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.FLOOR_DIV, 5, 3, 1); - testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.FLOOR_MOD, 6, 3, 0); - - testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.MIN, 5, 3, 3); - testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.MAX, 5, 3, 5); - testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.POW, 5, 3, 125); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SIGNUM, 0.55, 1); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.RAD, 5, 0.09); - testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.DEG, 5, 286.48); + private static Stream testSimpleTwoArgumentFunction() { + return Stream.of( + Arguments.of(TbRuleNodeMathFunctionType.ADD, 2.1, 2.2, 4.3), + Arguments.of(TbRuleNodeMathFunctionType.SUB, 2.1, 2.2, -0.1), + Arguments.of(TbRuleNodeMathFunctionType.MULT, 2.1, 2.0, 4.2), + Arguments.of(TbRuleNodeMathFunctionType.DIV, 4.2, 2.0, 2.1), + Arguments.of(TbRuleNodeMathFunctionType.ATAN2, 0.5, 0.3, 1.03), + Arguments.of(TbRuleNodeMathFunctionType.HYPOT, 4, 5, 6.4), + Arguments.of(TbRuleNodeMathFunctionType.FLOOR_DIV, 5, 3, 1), + Arguments.of(TbRuleNodeMathFunctionType.FLOOR_MOD, 6, 3, 0), + Arguments.of(TbRuleNodeMathFunctionType.MIN, 5, 3, 3), + Arguments.of(TbRuleNodeMathFunctionType.MAX, 5, 3, 5), + Arguments.of(TbRuleNodeMathFunctionType.POW, 5, 3, 125) + ); } - private void testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType function, double arg1, double arg2, double result) { - initMocks(); - + @ParameterizedTest + @MethodSource + public void testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType function, double arg1, double arg2, double result) { var node = initNode(function, new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", arg1).put("b", arg2).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", arg1).put("b", arg2).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - Mockito.verify(ctx, Mockito.timeout(5000).times(1)).tellSuccess(msgCaptor.capture()); + verify(ctx, timeout(TIMEOUT).times(1)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); - Assert.assertNotNull(resultMsg); - Assert.assertNotNull(resultMsg.getData()); + assertNotNull(resultMsg); + assertNotNull(resultMsg.getData()); var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); - Assert.assertTrue(resultJson.has("result")); - Assert.assertEquals(result, resultJson.get("result").asDouble(), 0d); + assertTrue(resultJson.has("result")); + assertEquals(result, resultJson.get("result").asDouble(), 0d); } - private void testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType function, double arg1, double result) { - initMocks(); + private static Stream testSimpleOneArgumentFunction() { + return Stream.of( + Arguments.of(TbRuleNodeMathFunctionType.SIN, Math.toRadians(30), 0.5), + Arguments.of(TbRuleNodeMathFunctionType.SIN, Math.toRadians(90), 1.0), + + Arguments.of(TbRuleNodeMathFunctionType.SINH, Math.toRadians(0), 0.0), + Arguments.of(TbRuleNodeMathFunctionType.COSH, Math.toRadians(0), 1.0), + + Arguments.of(TbRuleNodeMathFunctionType.COS, Math.toRadians(60), 0.5), + Arguments.of(TbRuleNodeMathFunctionType.COS, Math.toRadians(0), 1.0), + + Arguments.of(TbRuleNodeMathFunctionType.TAN, Math.toRadians(45), 1), + Arguments.of(TbRuleNodeMathFunctionType.TAN, Math.toRadians(0), 0), + Arguments.of(TbRuleNodeMathFunctionType.TANH, 90, 1), + + Arguments.of(TbRuleNodeMathFunctionType.ACOS, 0.5, 1.05), + Arguments.of(TbRuleNodeMathFunctionType.ASIN, 0.5, 0.52), + Arguments.of(TbRuleNodeMathFunctionType.ATAN, 0.5, 0.46), + + Arguments.of(TbRuleNodeMathFunctionType.EXP, 1, 2.72), + Arguments.of(TbRuleNodeMathFunctionType.EXPM1, 1, 1.72), + Arguments.of(TbRuleNodeMathFunctionType.ABS, -1, 1), + Arguments.of(TbRuleNodeMathFunctionType.SQRT, 4, 2), + Arguments.of(TbRuleNodeMathFunctionType.CBRT, 8, 2), + + Arguments.of(TbRuleNodeMathFunctionType.GET_EXP, 4, 2), + + Arguments.of(TbRuleNodeMathFunctionType.LOG, 4, 1.39), + Arguments.of(TbRuleNodeMathFunctionType.LOG10, 4, 0.6), + Arguments.of(TbRuleNodeMathFunctionType.LOG1P, 4, 1.61), + + Arguments.of(TbRuleNodeMathFunctionType.CEIL, 1.55, 2), + Arguments.of(TbRuleNodeMathFunctionType.FLOOR, 23.97, 23), + Arguments.of(TbRuleNodeMathFunctionType.SIGNUM, 0.55, 1), + Arguments.of(TbRuleNodeMathFunctionType.RAD, 5, 0.09), + Arguments.of(TbRuleNodeMathFunctionType.DEG, 5, 286.48) + ); + } + + @ParameterizedTest + @MethodSource + public void testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType function, double arg1, double result) { var node = initNode(function, new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", arg1).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", arg1).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); + verify(ctx, timeout(TIMEOUT).times(1)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); - Assert.assertNotNull(resultMsg); - Assert.assertNotNull(resultMsg.getData()); + assertNotNull(resultMsg); + assertNotNull(resultMsg.getData()); var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); - Assert.assertTrue(resultJson.has("result")); - Assert.assertEquals(result, resultJson.get("result").asDouble(), 0d); + assertTrue(resultJson.has("result")); + assertEquals(result, resultJson.get("result").asDouble(), 0d); } @Test @@ -292,19 +299,19 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); + verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); - Assert.assertNotNull(resultMsg); - Assert.assertNotNull(resultMsg.getData()); + assertNotNull(resultMsg); + assertNotNull(resultMsg.getData()); var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); - Assert.assertTrue(resultJson.has("result")); - Assert.assertEquals(4, resultJson.get("result").asInt()); + assertTrue(resultJson.has("result")); + assertEquals(4, resultJson.get("result").asInt()); } @Test @@ -315,20 +322,20 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); + verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); - Assert.assertNotNull(resultMsg); - Assert.assertNotNull(resultMsg.getData()); - Assert.assertNotNull(resultMsg.getMetaData()); + assertNotNull(resultMsg); + assertNotNull(resultMsg.getData()); + assertNotNull(resultMsg.getMetaData()); var result = resultMsg.getMetaData().getValue("result"); - Assert.assertNotNull(result); - Assert.assertEquals("4", result); + assertNotNull(result); + assertEquals("4", result); } @Test @@ -339,25 +346,25 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.TIME_SERIES, "b") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().toString()); - Mockito.when(attributesService.find(tenantId, originator, DataConstants.SERVER_SCOPE, "a")) + when(attributesService.find(tenantId, originator, DataConstants.SERVER_SCOPE, "a")) .thenReturn(Futures.immediateFuture(Optional.of(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("a", 2.0))))); - Mockito.when(tsService.findLatest(tenantId, originator, "b")) + when(tsService.findLatest(tenantId, originator, "b")) .thenReturn(Futures.immediateFuture(Optional.of(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("b", 2L))))); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); + verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); - Assert.assertNotNull(resultMsg); - Assert.assertNotNull(resultMsg.getData()); + assertNotNull(resultMsg); + assertNotNull(resultMsg.getData()); var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); - Assert.assertTrue(resultJson.has("result")); - Assert.assertEquals(4, resultJson.get("result").asInt()); + assertTrue(resultJson.has("result")); + assertEquals(4, resultJson.get("result").asInt()); } @Test @@ -367,19 +374,19 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 5).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); + verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); - Assert.assertNotNull(resultMsg); - Assert.assertNotNull(resultMsg.getData()); + assertNotNull(resultMsg); + assertNotNull(resultMsg.getData()); var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); - Assert.assertTrue(resultJson.has("result")); - Assert.assertEquals(2.236, resultJson.get("result").asDouble(), 0.0); + assertTrue(resultJson.has("result")); + assertEquals(2.236, resultJson.get("result").asDouble(), 0.0); } @Test @@ -389,19 +396,19 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 5).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); + verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); - Assert.assertNotNull(resultMsg); - Assert.assertNotNull(resultMsg.getData()); + assertNotNull(resultMsg); + assertNotNull(resultMsg.getData()); var result = resultMsg.getMetaData().getValue("result"); - Assert.assertNotNull(result); - Assert.assertEquals("2.236", result); + assertNotNull(result); + assertEquals("2.236", result); } @Test @@ -411,23 +418,23 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 5).toString()); - Mockito.when(telemetryService.saveAttrAndNotify(any(), any(), anyString(), anyString(), anyDouble())) + when(telemetryService.saveAttrAndNotify(any(), any(), anyString(), anyString(), anyDouble())) .thenReturn(Futures.immediateFuture(null)); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); - Mockito.verify(telemetryService, times(1)).saveAttrAndNotify(any(), any(), anyString(), anyString(), anyDouble()); + verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture()); + verify(telemetryService, times(1)).saveAttrAndNotify(any(), any(), anyString(), anyString(), anyDouble()); TbMsg resultMsg = msgCaptor.getValue(); - Assert.assertNotNull(resultMsg); - Assert.assertNotNull(resultMsg.getData()); + assertNotNull(resultMsg); + assertNotNull(resultMsg.getData()); var result = resultMsg.getMetaData().getValue("result"); - Assert.assertNotNull(result); - Assert.assertEquals("2.236", result); + assertNotNull(result); + assertEquals("2.236", result); } @Test @@ -437,22 +444,22 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); - Mockito.when(telemetryService.saveAndNotify(any(), any(), any(TsKvEntry.class))) + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 5).toString()); + when(telemetryService.saveAndNotify(any(), any(), any(TsKvEntry.class))) .thenReturn(Futures.immediateFuture(null)); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); - Mockito.verify(telemetryService, times(1)).saveAndNotify(any(), any(), any(TsKvEntry.class)); + verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture()); + verify(telemetryService, times(1)).saveAndNotify(any(), any(), any(TsKvEntry.class)); TbMsg resultMsg = msgCaptor.getValue(); - Assert.assertNotNull(resultMsg); - Assert.assertNotNull(resultMsg.getData()); + assertNotNull(resultMsg); + assertNotNull(resultMsg.getData()); var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); - Assert.assertTrue(resultJson.has("result")); - Assert.assertEquals(2.236, resultJson.get("result").asDouble(), 0.0); + assertTrue(resultJson.has("result")); + assertEquals(2.236, resultJson.get("result").asDouble(), 0.0); } @Test @@ -462,27 +469,27 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); - Mockito.when(telemetryService.saveAndNotify(any(), any(), any(TsKvEntry.class))) + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 5).toString()); + when(telemetryService.saveAndNotify(any(), any(), any(TsKvEntry.class))) .thenReturn(Futures.immediateFuture(null)); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); - Mockito.verify(telemetryService, times(1)).saveAndNotify(any(), any(), any(TsKvEntry.class)); + verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture()); + verify(telemetryService, times(1)).saveAndNotify(any(), any(), any(TsKvEntry.class)); TbMsg resultMsg = msgCaptor.getValue(); - Assert.assertNotNull(resultMsg); - Assert.assertNotNull(resultMsg.getData()); + assertNotNull(resultMsg); + assertNotNull(resultMsg.getData()); var resultMetadata = resultMsg.getMetaData().getValue("result"); var resultData = JacksonUtil.toJsonNode(resultMsg.getData()); - Assert.assertTrue(resultData.has("result")); - Assert.assertEquals(2.236, resultData.get("result").asDouble(), 0.0); + assertTrue(resultData.has("result")); + assertEquals(2.236, resultData.get("result").asDouble(), 0.0); - Assert.assertNotNull(resultMetadata); - Assert.assertEquals("2.236", resultMetadata); + assertNotNull(resultMetadata); + assertEquals("2.236", resultMetadata); } @Test @@ -493,18 +500,18 @@ public class TbMathNodeTest { new TbMathResult(TbMathArgumentType.MESSAGE_METADATA, "result", 3, false, false, null), tbMathArgument ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 10).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 10).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); + verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); - Assert.assertNotNull(resultMsg); - Assert.assertNotNull(resultMsg.getData()); + assertNotNull(resultMsg); + assertNotNull(resultMsg.getData()); var result = resultMsg.getMetaData().getValue("result"); - Assert.assertNotNull(result); - Assert.assertEquals("2.236", result); + assertNotNull(result); + assertEquals("2.236", result); } @Test @@ -513,11 +520,11 @@ public class TbMathNodeTest { new TbMathResult(TbMathArgumentType.TIME_SERIES, "result", 3, true, false, DataConstants.SERVER_SCOPE), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 10).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 10).toString()); Throwable thrown = assertThrows(RuntimeException.class, () -> { node.onMsg(ctx, msg); }); - Assert.assertNotNull(thrown.getMessage()); + assertNotNull(thrown.getMessage()); } @Test @@ -527,10 +534,93 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), "[]"); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); Throwable thrown = assertThrows(RuntimeException.class, () -> { node.onMsg(ctx, msg); }); - Assert.assertNotNull(thrown.getMessage()); + assertNotNull(thrown.getMessage()); } + + @Test + public void testExp4j_concurrent() { + TbMathNode node = spy(initNodeWithCustomFunction("2a+3b", + new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), + new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"), + new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") + )); + EntityId originatorSlow = DeviceId.fromString("7f01170d-6bba-419c-b95c-2b4c3ba32f30"); + EntityId originatorFast = DeviceId.fromString("c45360ff-7906-4102-a2ae-3495a86168d0"); + CountDownLatch slowProcessingLatch = new CountDownLatch(1); + + List slowMsgList = IntStream.range(0, 5) + .mapToObj(x -> TbMsg.newMsg("TEST", originatorSlow, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString())) + .collect(Collectors.toList()); + List fastMsgList = IntStream.range(0, 2) + .mapToObj(x -> TbMsg.newMsg("TEST", originatorFast, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString())) + .collect(Collectors.toList()); + + assertThat(slowMsgList.size()).as("slow msgs >= rule-dispatcher pool size").isGreaterThanOrEqualTo(RULE_DISPATCHER_POOL_SIZE); + + log.debug("rule-dispatcher [{}], db-callback [{}], slowMsg [{}], fastMsg [{}]", RULE_DISPATCHER_POOL_SIZE, DB_CALLBACK_POOL_SIZE, slowMsgList.size(), fastMsgList.size()); + + willAnswer(invocation -> { + TbMsg msg = invocation.getArgument(1); + log.debug("\uD83D\uDC0C processMsgAsync slow originator [{}][{}]", msg.getOriginator(), msg); + try { + assertThat(slowProcessingLatch.await(30, TimeUnit.SECONDS)).as("await on slowProcessingLatch").isTrue(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return invocation.callRealMethod(); + }).given(node).processMsgAsync(eq(ctx), argThat(slowMsgList::contains)); + + willAnswer(invocation -> { + TbMsg msg = invocation.getArgument(1); + log.debug("\u26A1\uFE0F processMsgAsync FAST originator [{}][{}]", msg.getOriginator(), msg); + return invocation.callRealMethod(); + }).given(node).processMsgAsync(eq(ctx), argThat(fastMsgList::contains)); + + willAnswer(invocation -> { + TbMsg msg = invocation.getArgument(1); + log.debug("submit slow originator onMsg [{}][{}]", msg.getOriginator(), msg); + return invocation.callRealMethod(); + }).given(node).onMsg(eq(ctx), argThat(slowMsgList::contains)); + + willAnswer(invocation -> { + TbMsg msg = invocation.getArgument(1); + log.debug("submit FAST originator onMsg [{}][{}]", msg.getOriginator(), msg); + return invocation.callRealMethod(); + }).given(node).onMsg(eq(ctx), argThat(fastMsgList::contains)); + + // submit slow msg may block all rule engine dispatcher threads + slowMsgList.forEach(msg -> ruleEngineDispatcherExecutor.executeAsync(() -> node.onMsg(ctx, msg))); + // wait until dispatcher threads started with all slowMsg + verify(node, timeout(TIMEOUT).times(slowMsgList.size())).onMsg(eq(ctx), argThat(slowMsgList::contains)); + + // submit fast have to return immediately + fastMsgList.forEach(msg -> ruleEngineDispatcherExecutor.executeAsync(() -> node.onMsg(ctx, msg))); + // wait until all fast messages processed + verify(ctx, timeout(TIMEOUT).times(fastMsgList.size())).tellSuccess(any()); + + slowProcessingLatch.countDown(); + + verify(ctx, timeout(TIMEOUT).times(fastMsgList.size() + slowMsgList.size())).tellSuccess(any()); + + verify(ctx, never()).tellFailure(any(), any()); + } + + static class RuleDispatcherExecutor extends AbstractListeningExecutor { + @Override + protected int getThreadPollSize() { + return RULE_DISPATCHER_POOL_SIZE; + } + } + + static class DBCallbackExecutor extends AbstractListeningExecutor { + @Override + protected int getThreadPollSize() { + return DB_CALLBACK_POOL_SIZE; + } + } + } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java index 86765446e0..0fd8793e82 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java @@ -16,10 +16,8 @@ package org.thingsboard.rule.engine.metadata; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import org.assertj.core.api.Assertions; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,6 +27,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; @@ -41,13 +40,14 @@ import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.util.List; import java.util.UUID; -import java.util.concurrent.Callable; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -70,21 +70,7 @@ public class CalculateDeltaNodeTest { private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -117,13 +103,13 @@ public class CalculateDeltaNodeTest { public void givenInvalidMsgType_whenOnMsg_thenShouldTellNextOther() { // GIVEN var msgData = "{\"pulseCounter\": 42}"; - var msg = TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN node.onMsg(ctxMock, msg); // THEN - verify(ctxMock, times(1)).tellNext(eq(msg), eq("Other")); + verify(ctxMock, times(1)).tellNext(eq(msg), eq(TbNodeConnectionType.OTHER)); verify(ctxMock, never()).tellSuccess(any()); verify(ctxMock, never()).tellFailure(any(), any()); } @@ -131,14 +117,13 @@ public class CalculateDeltaNodeTest { @Test public void givenInvalidMsgDataType_whenOnMsg_thenShouldTellNextOther() { // GIVEN - var msgData = "[]"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN node.onMsg(ctxMock, msg); // THEN - verify(ctxMock, times(1)).tellNext(eq(msg), eq("Other")); + verify(ctxMock, times(1)).tellNext(eq(msg), eq(TbNodeConnectionType.OTHER)); verify(ctxMock, never()).tellSuccess(any()); verify(ctxMock, never()).tellFailure(any(), any()); } @@ -147,13 +132,13 @@ public class CalculateDeltaNodeTest { @Test public void givenInputKeyIsNotPresent_whenOnMsg_thenShouldTellNextOther() { // GIVEN - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "{}"); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); // WHEN node.onMsg(ctxMock, msg); // THEN - verify(ctxMock, times(1)).tellNext(eq(msg), eq("Other")); + verify(ctxMock, times(1)).tellNext(eq(msg), eq(TbNodeConnectionType.OTHER)); verify(ctxMock, never()).tellSuccess(any()); verify(ctxMock, never()).tellFailure(any(), any()); } @@ -171,7 +156,7 @@ public class CalculateDeltaNodeTest { mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 40.5))); var msgData = "{\"temperature\": 42,\"airPressure\":123}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN node.onMsg(ctxMock, msg); @@ -201,7 +186,7 @@ public class CalculateDeltaNodeTest { mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("temperature", 40L))); var msgData = "{\"temperature\": 42,\"airPressure\":123}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN node.onMsg(ctxMock, msg); @@ -231,7 +216,7 @@ public class CalculateDeltaNodeTest { mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("temperature", "40.0"))); var msgData = "{\"temperature\": 42,\"airPressure\":123}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN node.onMsg(ctxMock, msg); @@ -265,7 +250,7 @@ public class CalculateDeltaNodeTest { var msgData = "{\"temperature\": 42,\"airPressure\":123}"; var firstMsgMetaData = new TbMsgMetaData(); firstMsgMetaData.putValue("ts", String.valueOf(3L)); - var firstMsg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, firstMsgMetaData, msgData); + var firstMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, firstMsgMetaData, msgData); // WHEN node.onMsg(ctxMock, firstMsg); @@ -289,7 +274,7 @@ public class CalculateDeltaNodeTest { var secondMsgMetaData = new TbMsgMetaData(); secondMsgMetaData.putValue("ts", String.valueOf(6L)); - var secondMsg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, secondMsgMetaData, msgData); + var secondMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, secondMsgMetaData, msgData); // WHEN node.onMsg(ctxMock, secondMsg); @@ -320,7 +305,7 @@ public class CalculateDeltaNodeTest { mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", null))); var msgData = "{\"temperature\": 42,\"airPressure\":123}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN node.onMsg(ctxMock, msg); @@ -348,7 +333,7 @@ public class CalculateDeltaNodeTest { mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("pulseCounter", 200L))); var msgData = "{\"pulseCounter\":\"123\"}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN node.onMsg(ctxMock, msg); @@ -368,7 +353,33 @@ public class CalculateDeltaNodeTest { assertEquals(msg, actualMsgCaptor.getValue()); assertInstanceOf(IllegalArgumentException.class, actualException); assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void givenNegativeDeltaAndTellFailureIfNegativeDeltaFalse_whenOnMsg_thenShouldTellSuccess() throws TbNodeException { + // GIVEN + config.setTellFailureIfDeltaIsNegative(false); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, nodeConfiguration); + + mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("pulseCounter", 200L))); + var msgData = "{\"pulseCounter\":\"123\"}"; + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + + verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); + verify(ctxMock, never()).tellFailure(any(), any()); + verify(ctxMock, never()).tellNext(any(), anyString()); + verify(ctxMock, never()).tellNext(any(), anySet()); + + String expectedMsgData = "{\"pulseCounter\":\"123\",\"delta\":-77}"; + assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); } @Test @@ -377,7 +388,7 @@ public class CalculateDeltaNodeTest { mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("pulseCounter", "high"))); var msgData = "{\"pulseCounter\":\"123\"}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN-THEN Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) @@ -391,7 +402,7 @@ public class CalculateDeltaNodeTest { mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry("pulseCounter", false))); var msgData = "{\"pulseCounter\":true}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN-THEN Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) @@ -405,7 +416,7 @@ public class CalculateDeltaNodeTest { mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new JsonDataEntry("pulseCounter", "{\"isActive\":false}"))); var msgData = "{\"pulseCounter\":{\"isActive\":true}}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN-THEN Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java index 4c9d138236..c5044eab37 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java @@ -28,10 +28,13 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -51,7 +54,6 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.thingsboard.server.common.data.security.DeviceCredentialsType.ACCESS_TOKEN; @ExtendWith(MockitoExtension.class) public class TbFetchDeviceCredentialsNodeTest { @@ -82,13 +84,13 @@ public class TbFetchDeviceCredentialsNodeTest { @Test void givenDefaultConfig_whenInit_thenOK() { assertThat(node.config).isEqualTo(config); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); } @Test void givenDefaultConfig_whenVerify_thenOK() { var defaultConfig = new TbFetchDeviceCredentialsNodeConfiguration().defaultConfiguration(); - assertThat(defaultConfig.getFetchTo()).isEqualTo(FetchTo.METADATA); + assertThat(defaultConfig.getFetchTo()).isEqualTo(TbMsgSource.METADATA); } @Test @@ -97,7 +99,7 @@ public class TbFetchDeviceCredentialsNodeTest { doReturn(deviceCredentialsServiceMock).when(ctxMock).getDeviceCredentialsService(); doAnswer(invocation -> { DeviceCredentials deviceCredentials = new DeviceCredentials(); - deviceCredentials.setCredentialsType(ACCESS_TOKEN); + deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); return deviceCredentials; }).when(deviceCredentialsServiceMock).findDeviceCredentialsByDeviceId(any(), any()); doAnswer(invocation -> JacksonUtil.newObjectNode()).when(deviceCredentialsServiceMock).toCredentialsInfo(any()); @@ -171,7 +173,7 @@ public class TbFetchDeviceCredentialsNodeTest { final var metaData = new TbMsgMetaData(mdMap); final String data = "{\"TestAttribute_1\": \"humidity\", \"TestAttribute_2\": \"voltage\"}"; - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, metaData, data, callbackMock); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, metaData, data, callbackMock); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java index 5146f2f1e9..6c8e83a22f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java @@ -31,6 +31,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -41,6 +42,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -125,7 +127,7 @@ public class TbGetAttributesNodeTest { @Test public void givenFetchAttributesToMetadata_whenOnMsg_thenShouldTellSuccess() throws Exception { // GIVEN - node = initNode(FetchTo.METADATA, false, false); + node = initNode(TbMsgSource.METADATA, false, false); var msg = getTbMsg(ORIGINATOR); // WHEN @@ -134,17 +136,17 @@ public class TbGetAttributesNodeTest { // THEN var resultMsg = checkMsg(true); - checkAttributes(resultMsg, FetchTo.METADATA, "cs_", clientAttributes); - checkAttributes(resultMsg, FetchTo.METADATA, "ss_", serverAttributes); - checkAttributes(resultMsg, FetchTo.METADATA, "shared_", sharedAttributes); + checkAttributes(resultMsg, TbMsgSource.METADATA, "cs_", clientAttributes); + checkAttributes(resultMsg, TbMsgSource.METADATA, "ss_", serverAttributes); + checkAttributes(resultMsg, TbMsgSource.METADATA, "shared_", sharedAttributes); - checkTs(resultMsg, FetchTo.METADATA, false, tsKeys); + checkTs(resultMsg, TbMsgSource.METADATA, false, tsKeys); } @Test public void givenFetchLatestTimeseriesToMetadata_whenOnMsg_thenShouldTellSuccess() throws Exception { // GIVEN - node = initNode(FetchTo.METADATA, true, false); + node = initNode(TbMsgSource.METADATA, true, false); var msg = getTbMsg(ORIGINATOR); // WHEN @@ -153,17 +155,17 @@ public class TbGetAttributesNodeTest { // THEN var resultMsg = checkMsg(true); - checkAttributes(resultMsg, FetchTo.METADATA, "cs_", clientAttributes); - checkAttributes(resultMsg, FetchTo.METADATA, "ss_", serverAttributes); - checkAttributes(resultMsg, FetchTo.METADATA, "shared_", sharedAttributes); + checkAttributes(resultMsg, TbMsgSource.METADATA, "cs_", clientAttributes); + checkAttributes(resultMsg, TbMsgSource.METADATA, "ss_", serverAttributes); + checkAttributes(resultMsg, TbMsgSource.METADATA, "shared_", sharedAttributes); - checkTs(resultMsg, FetchTo.METADATA, true, tsKeys); + checkTs(resultMsg, TbMsgSource.METADATA, true, tsKeys); } @Test public void givenFetchAttributesToData_whenOnMsg_thenShouldTellSuccess() throws Exception { // GIVEN - node = initNode(FetchTo.DATA, false, false); + node = initNode(TbMsgSource.DATA, false, false); var msg = getTbMsg(ORIGINATOR); // WHEN @@ -172,17 +174,17 @@ public class TbGetAttributesNodeTest { // THEN var resultMsg = checkMsg(true); - checkAttributes(resultMsg, FetchTo.DATA, "cs_", clientAttributes); - checkAttributes(resultMsg, FetchTo.DATA, "ss_", serverAttributes); - checkAttributes(resultMsg, FetchTo.DATA, "shared_", sharedAttributes); + checkAttributes(resultMsg, TbMsgSource.DATA, "cs_", clientAttributes); + checkAttributes(resultMsg, TbMsgSource.DATA, "ss_", serverAttributes); + checkAttributes(resultMsg, TbMsgSource.DATA, "shared_", sharedAttributes); - checkTs(resultMsg, FetchTo.DATA, false, tsKeys); + checkTs(resultMsg, TbMsgSource.DATA, false, tsKeys); } @Test public void givenFetchLatestTimeseriesToData_whenOnMsg_thenShouldTellSuccess() throws Exception { // GIVEN - node = initNode(FetchTo.DATA, true, false); + node = initNode(TbMsgSource.DATA, true, false); var msg = getTbMsg(ORIGINATOR); // WHEN @@ -191,17 +193,17 @@ public class TbGetAttributesNodeTest { // THEN var resultMsg = checkMsg(true); - checkAttributes(resultMsg, FetchTo.DATA, "cs_", clientAttributes); - checkAttributes(resultMsg, FetchTo.DATA, "ss_", serverAttributes); - checkAttributes(resultMsg, FetchTo.DATA, "shared_", sharedAttributes); + checkAttributes(resultMsg, TbMsgSource.DATA, "cs_", clientAttributes); + checkAttributes(resultMsg, TbMsgSource.DATA, "ss_", serverAttributes); + checkAttributes(resultMsg, TbMsgSource.DATA, "shared_", sharedAttributes); - checkTs(resultMsg, FetchTo.DATA, true, tsKeys); + checkTs(resultMsg, TbMsgSource.DATA, true, tsKeys); } @Test public void givenFetchAttributesToMetadata_whenOnMsg_thenShouldTellFailure() throws Exception { // GIVEN - node = initNode(FetchTo.METADATA, false, true); + node = initNode(TbMsgSource.METADATA, false, true); var msg = getTbMsg(ORIGINATOR); // WHEN @@ -210,17 +212,17 @@ public class TbGetAttributesNodeTest { // THEN var actualMsg = checkMsg(false); - checkAttributes(actualMsg, FetchTo.METADATA, "cs_", clientAttributes); - checkAttributes(actualMsg, FetchTo.METADATA, "ss_", serverAttributes); - checkAttributes(actualMsg, FetchTo.METADATA, "shared_", sharedAttributes); + checkAttributes(actualMsg, TbMsgSource.METADATA, "cs_", clientAttributes); + checkAttributes(actualMsg, TbMsgSource.METADATA, "ss_", serverAttributes); + checkAttributes(actualMsg, TbMsgSource.METADATA, "shared_", sharedAttributes); - checkTs(actualMsg, FetchTo.METADATA, false, tsKeys); + checkTs(actualMsg, TbMsgSource.METADATA, false, tsKeys); } @Test public void givenFetchLatestTimeseriesToData_whenOnMsg_thenShouldTellFailure() throws Exception { // GIVEN - node = initNode(FetchTo.DATA, true, true); + node = initNode(TbMsgSource.DATA, true, true); var msg = getTbMsg(ORIGINATOR); // WHEN @@ -229,18 +231,18 @@ public class TbGetAttributesNodeTest { // THEN var actualMsg = checkMsg(false); - checkAttributes(actualMsg, FetchTo.DATA, "cs_", clientAttributes); - checkAttributes(actualMsg, FetchTo.DATA, "ss_", serverAttributes); - checkAttributes(actualMsg, FetchTo.DATA, "shared_", sharedAttributes); + checkAttributes(actualMsg, TbMsgSource.DATA, "cs_", clientAttributes); + checkAttributes(actualMsg, TbMsgSource.DATA, "ss_", serverAttributes); + checkAttributes(actualMsg, TbMsgSource.DATA, "shared_", sharedAttributes); - checkTs(actualMsg, FetchTo.DATA, true, tsKeys); + checkTs(actualMsg, TbMsgSource.DATA, true, tsKeys); } @Test public void givenFetchLatestTimeseriesToDataAndDataIsNotJsonObject_whenOnMsg_thenException() throws Exception { // GIVEN - node = initNode(FetchTo.DATA, true, true); - var msg = TbMsg.newMsg("TEST", ORIGINATOR, new TbMsgMetaData(), "[]"); + node = initNode(TbMsgSource.DATA, true, true); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -267,6 +269,22 @@ public class TbGetAttributesNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } + @Test + public void givenOldConfigWithNoFetchToDataProperty_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { + var defaultConfig = new TbGetAttributesNodeConfiguration().defaultConfiguration(); + var node = new TbGetAttributesNode(); + String oldConfig = "{\"clientAttributeNames\":[]," + + "\"sharedAttributeNames\":[]," + + "\"serverAttributeNames\":[]," + + "\"latestTsKeyNames\":[]," + + "\"tellFailureIfAbsent\":true," + + "\"getLatestValueWithTs\":false}"; + JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); + TbPair upgrade = node.upgrade(0, configJson); + Assertions.assertTrue(upgrade.getFirst()); + Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); + } + private TbMsg checkMsg(boolean checkSuccess) { var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); if (checkSuccess) { @@ -288,15 +306,15 @@ public class TbGetAttributesNodeTest { return resultMsg; } - private void checkAttributes(TbMsg actualMsg, FetchTo fetchTo, String prefix, List attributes) { + private void checkAttributes(TbMsg actualMsg, TbMsgSource fetchTo, String prefix, List attributes) { var msgData = JacksonUtil.toJsonNode(actualMsg.getData()); attributes.stream() .filter(attribute -> !attribute.equals("unknown")) .forEach(attribute -> { String result = null; - if (FetchTo.DATA.equals(fetchTo)) { + if (TbMsgSource.DATA.equals(fetchTo)) { result = msgData.get(prefix + attribute).asText(); - } else if (FetchTo.METADATA.equals(fetchTo)) { + } else if (TbMsgSource.METADATA.equals(fetchTo)) { result = actualMsg.getMetaData().getValue(prefix + attribute); } assertNotNull(result); @@ -304,7 +322,7 @@ public class TbGetAttributesNodeTest { }); } - private void checkTs(TbMsg actualMsg, FetchTo fetchTo, boolean getLatestValueWithTs, List tsKeys) { + private void checkTs(TbMsg actualMsg, TbMsgSource fetchTo, boolean getLatestValueWithTs, List tsKeys) { var msgData = JacksonUtil.toJsonNode(actualMsg.getData()); long value = 1L; for (var key : tsKeys) { @@ -318,9 +336,9 @@ public class TbGetAttributesNodeTest { } else { expectedValue = "{\"data\":" + value + "}"; } - if (FetchTo.DATA.equals(fetchTo)) { + if (TbMsgSource.DATA.equals(fetchTo)) { actualValue = JacksonUtil.toString(msgData.get(key)); - } else if (FetchTo.METADATA.equals(fetchTo)) { + } else if (TbMsgSource.METADATA.equals(fetchTo)) { actualValue = actualMsg.getMetaData().getValue(key); } assertNotNull(actualValue); @@ -329,7 +347,7 @@ public class TbGetAttributesNodeTest { } } - private TbGetAttributesNode initNode(FetchTo fetchTo, boolean getLatestValueWithTs, boolean isTellFailureIfAbsent) throws TbNodeException { + private TbGetAttributesNode initNode(TbMsgSource fetchTo, boolean getLatestValueWithTs, boolean isTellFailureIfAbsent) throws TbNodeException { var config = new TbGetAttributesNodeConfiguration(); config.setClientAttributeNames(List.of("client_attr_1", "client_attr_2", "${client_attr_metadata}", "unknown")); config.setServerAttributeNames(List.of("server_attr_1", "server_attr_2", "${server_attr_metadata}", "unknown")); @@ -353,7 +371,7 @@ public class TbGetAttributesNodeTest { msgMetaData.putValue("client_attr_metadata", "client_attr_3"); msgMetaData.putValue("server_attr_metadata", "server_attr_3"); - return TbMsg.newMsg("TEST", entityId, msgMetaData, JacksonUtil.toString(msgData)); + return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, msgMetaData, JacksonUtil.toString(msgData)); } private List getAttributeNames(String prefix) { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java index 156c3f03d7..291614464a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java @@ -17,9 +17,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,10 +28,13 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; @@ -48,6 +49,7 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -62,7 +64,6 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.UUID; -import java.util.concurrent.Callable; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -76,7 +77,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; @ExtendWith(MockitoExtension.class) public class TbGetCustomerAttributeNodeTest { @@ -84,21 +84,7 @@ public class TbGetCustomerAttributeNodeTest { private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); private static final CustomerId CUSTOMER_ID = new CustomerId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -176,7 +162,7 @@ public class TbGetCustomerAttributeNodeTest { assertThat(node.config).isEqualTo(config); assertThat(config.getDataMapping()).isEqualTo(Map.of("alarmThreshold", "threshold")); assertThat(config.getDataToFetch()).isEqualTo(DataToFetch.ATTRIBUTES); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); } @Test @@ -187,7 +173,7 @@ public class TbGetCustomerAttributeNodeTest { "sourceAttr2", "targetKey2", "sourceAttr3", "targetKey3")); config.setDataToFetch(DataToFetch.LATEST_TELEMETRY); - config.setFetchTo(FetchTo.DATA); + config.setFetchTo(TbMsgSource.DATA); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); // WHEN @@ -200,7 +186,7 @@ public class TbGetCustomerAttributeNodeTest { "sourceAttr2", "targetKey2", "sourceAttr3", "targetKey3")); assertThat(config.getDataToFetch()).isEqualTo(DataToFetch.LATEST_TELEMETRY); - assertThat(node.fetchTo).isEqualTo(FetchTo.DATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.DATA); } @Test @@ -222,8 +208,8 @@ public class TbGetCustomerAttributeNodeTest { @Test public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { // GIVEN - node.fetchTo = FetchTo.DATA; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); + node.fetchTo = TbMsgSource.DATA; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -238,7 +224,7 @@ public class TbGetCustomerAttributeNodeTest { // GIVEN var userId = new UserId(UUID.randomUUID()); - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", userId, new TbMsgMetaData(), "{}"); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, userId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -276,7 +262,7 @@ public class TbGetCustomerAttributeNodeTest { var device = new Device(new DeviceId(UUID.randomUUID())); device.setCustomerId(CUSTOMER_ID); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.ATTRIBUTES, device.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.ATTRIBUTES, device.getId()); List attributesList = List.of( new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L), @@ -291,7 +277,7 @@ public class TbGetCustomerAttributeNodeTest { doReturn(device).when(deviceServiceMock).findDeviceById(eq(TENANT_ID), eq(device.getId())); when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); - when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(DataConstants.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) .thenReturn(Futures.immediateFuture(attributesList)); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); @@ -323,7 +309,7 @@ public class TbGetCustomerAttributeNodeTest { var user = new User(new UserId(UUID.randomUUID())); user.setCustomerId(CUSTOMER_ID); - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.ATTRIBUTES, user.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.ATTRIBUTES, user.getId()); List attributesList = List.of( new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L), @@ -338,7 +324,7 @@ public class TbGetCustomerAttributeNodeTest { doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(eq(TENANT_ID), eq(user.getId())); when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); - when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(DataConstants.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) .thenReturn(Futures.immediateFuture(attributesList)); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); @@ -369,7 +355,7 @@ public class TbGetCustomerAttributeNodeTest { // GIVEN var customer = new Customer(new CustomerId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.LATEST_TELEMETRY, customer.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.LATEST_TELEMETRY, customer.getId()); List timeseriesList = List.of( new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")), @@ -413,7 +399,7 @@ public class TbGetCustomerAttributeNodeTest { var asset = new Asset(new AssetId(UUID.randomUUID())); asset.setCustomerId(new CustomerId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.LATEST_TELEMETRY, asset.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.LATEST_TELEMETRY, asset.getId()); List timeseriesList = List.of( new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")), @@ -465,7 +451,7 @@ public class TbGetCustomerAttributeNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } - private void prepareMsgAndConfig(FetchTo fetchTo, DataToFetch dataToFetch, EntityId originator) { + private void prepareMsgAndConfig(TbMsgSource fetchTo, DataToFetch dataToFetch, EntityId originator) { config.setDataMapping(Map.of( "sourceKey1", "targetKey1", "${metaDataPattern1}", "$[messageBodyPattern1]", @@ -482,7 +468,7 @@ public class TbGetCustomerAttributeNodeTest { var msgData = "{\"temp\":42,\"humidity\":77,\"messageBodyPattern1\":\"targetKey2\",\"messageBodyPattern2\":\"sourceKey3\"}"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", originator, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, msgMetaData, msgData); } @RequiredArgsConstructor diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java index 2830dddaeb..51625eafe7 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java @@ -17,8 +17,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,10 +26,12 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.util.ContactBasedEntityDetails; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; @@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -60,7 +61,6 @@ import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import java.util.UUID; -import java.util.concurrent.Callable; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -76,21 +76,7 @@ public class TbGetCustomerDetailsNodeTest { private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -148,14 +134,14 @@ public class TbGetCustomerDetailsNodeTest { @Test public void givenDefaultConfig_whenInit_thenOK() { assertThat(config.getDetailsList()).isEqualTo(Collections.emptyList()); - assertThat(config.getFetchTo()).isEqualTo(FetchTo.DATA); + assertThat(config.getFetchTo()).isEqualTo(TbMsgSource.DATA); } @Test public void givenCustomConfig_whenInit_thenOK() throws TbNodeException { // GIVEN config.setDetailsList(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE)); - config.setFetchTo(FetchTo.METADATA); + config.setFetchTo(TbMsgSource.METADATA); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); // WHEN @@ -164,15 +150,15 @@ public class TbGetCustomerDetailsNodeTest { // THEN assertThat(node.config).isEqualTo(config); assertThat(config.getDetailsList()).isEqualTo(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE)); - assertThat(config.getFetchTo()).isEqualTo(FetchTo.METADATA); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(config.getFetchTo()).isEqualTo(TbMsgSource.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); } @Test public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { // GIVEN - node.fetchTo = FetchTo.DATA; - msg = TbMsg.newMsg("SOME_MESSAGE_TYPE", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); + node.fetchTo = TbMsgSource.DATA; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -189,7 +175,7 @@ public class TbGetCustomerDetailsNodeTest { device.setId(new DeviceId(UUID.randomUUID())); device.setCustomerId(customer.getId()); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.values()), device.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.values()), device.getId()); when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); when(deviceServiceMock.findDeviceByIdAsync(eq(TENANT_ID), eq(device.getId()))).thenReturn(Futures.immediateFuture(device)); @@ -231,7 +217,7 @@ public class TbGetCustomerDetailsNodeTest { asset.setId(new AssetId(UUID.randomUUID())); asset.setCustomerId(customer.getId()); - prepareMsgAndConfig(FetchTo.METADATA, List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.TITLE, ContactBasedEntityDetails.PHONE), asset.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.TITLE, ContactBasedEntityDetails.PHONE), asset.getId()); when(ctxMock.getAssetService()).thenReturn(assetServiceMock); when(assetServiceMock.findAssetByIdAsync(eq(TENANT_ID), eq(asset.getId()))).thenReturn(Futures.immediateFuture(asset)); @@ -269,7 +255,7 @@ public class TbGetCustomerDetailsNodeTest { user.setId(new UserId(UUID.randomUUID())); user.setCustomerId(customer.getId()); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), user.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), user.getId()); when(ctxMock.getUserService()).thenReturn(userServiceMock); when(userServiceMock.findUserByIdAsync(eq(TENANT_ID), eq(user.getId()))).thenReturn(Futures.immediateFuture(user)); @@ -298,7 +284,7 @@ public class TbGetCustomerDetailsNodeTest { edge.setId(new EdgeId(UUID.randomUUID())); edge.setCustomerId(customer.getId()); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), edge.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), edge.getId()); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -330,7 +316,7 @@ public class TbGetCustomerDetailsNodeTest { edge.setId(new EdgeId(UUID.randomUUID())); edge.setCustomerId(customer.getId()); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), edge.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), edge.getId()); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -359,7 +345,7 @@ public class TbGetCustomerDetailsNodeTest { device.setId(new DeviceId(UUID.randomUUID())); device.setName("Thermostat"); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), device.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), device.getId()); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -397,7 +383,7 @@ public class TbGetCustomerDetailsNodeTest { device.setId(new DeviceId(UUID.randomUUID())); device.setCustomerId(customer.getId()); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ADDITIONAL_INFO), device.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ADDITIONAL_INFO), device.getId()); when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); when(deviceServiceMock.findDeviceByIdAsync(eq(TENANT_ID), eq(device.getId()))).thenReturn(Futures.immediateFuture(device)); @@ -425,7 +411,7 @@ public class TbGetCustomerDetailsNodeTest { var dashboard = new Dashboard(); dashboard.setId(new DashboardId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.METADATA, List.of(ContactBasedEntityDetails.STATE), dashboard.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, List.of(ContactBasedEntityDetails.STATE), dashboard.getId()); // WHEN node.onMsg(ctxMock, msg); @@ -458,7 +444,7 @@ public class TbGetCustomerDetailsNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } - private void prepareMsgAndConfig(FetchTo fetchTo, List detailsList, EntityId originator) { + private void prepareMsgAndConfig(TbMsgSource fetchTo, List detailsList, EntityId originator) { config.setDetailsList(detailsList); config.setFetchTo(fetchTo); @@ -471,7 +457,7 @@ public class TbGetCustomerDetailsNodeTest { var msgData = "{\"dataKey1\":123,\"dataKey2\":\"dataValue2\"}"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", originator, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, msgMetaData, msgData); } private void mockFindCustomer() { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeTest.java index 82e24226f5..bdb431227b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeTest.java @@ -42,4 +42,22 @@ public class TbGetDeviceAttrNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } + @Test + public void givenOldConfigWithNoFetchToDataProperty_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { + var defaultConfig = new TbGetDeviceAttrNodeConfiguration().defaultConfiguration(); + var node = new TbGetDeviceAttrNode(); + String oldConfig = "{\"clientAttributeNames\":[]," + + "\"sharedAttributeNames\":[]," + + "\"serverAttributeNames\":[]," + + "\"latestTsKeyNames\":[]," + + "\"tellFailureIfAbsent\":true," + + "\"getLatestValueWithTs\":false," + + "\"deviceRelationsQuery\":{\"direction\":\"FROM\",\"maxLevel\":1,\"relationType\":\"Contains\",\"deviceTypes\":[\"default\"]," + + "\"fetchLastLevelOnly\":false}}"; + JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); + TbPair upgrade = node.upgrade(0, configJson); + Assertions.assertTrue(upgrade.getFirst()); + Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); + } + } \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java index 4f03fe54c0..d681deb9c0 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java @@ -16,9 +16,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,13 +25,16 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -43,7 +43,6 @@ import org.thingsboard.server.dao.device.DeviceService; import java.util.Collections; import java.util.Map; import java.util.UUID; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import static org.assertj.core.api.Assertions.assertThat; @@ -60,21 +59,7 @@ public class TbGetOriginatorFieldsNodeTest { private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId DUMMY_TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -116,8 +101,8 @@ public class TbGetOriginatorFieldsNodeTest { "name", "originatorName", "type", "originatorType")); assertThat(config.isIgnoreNullStrings()).isEqualTo(false); - assertThat(config.getFetchTo()).isEqualTo(FetchTo.METADATA); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(config.getFetchTo()).isEqualTo(TbMsgSource.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); } @Test @@ -128,7 +113,7 @@ public class TbGetOriginatorFieldsNodeTest { "title", "originatorTitle", "country", "originatorCountry")); config.setIgnoreNullStrings(true); - config.setFetchTo(FetchTo.DATA); + config.setFetchTo(TbMsgSource.DATA); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); // WHEN @@ -141,15 +126,15 @@ public class TbGetOriginatorFieldsNodeTest { "title", "originatorTitle", "country", "originatorCountry")); assertThat(config.isIgnoreNullStrings()).isEqualTo(true); - assertThat(config.getFetchTo()).isEqualTo(FetchTo.DATA); - assertThat(node.fetchTo).isEqualTo(FetchTo.DATA); + assertThat(config.getFetchTo()).isEqualTo(TbMsgSource.DATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.DATA); } @Test public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { // GIVEN - node.fetchTo = FetchTo.DATA; - msg = TbMsg.newMsg("SOME_MESSAGE_TYPE", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); + node.fetchTo = TbMsgSource.DATA; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -172,13 +157,55 @@ public class TbGetOriginatorFieldsNodeTest { "type", "originatorType", "label", "originatorLabel")); config.setIgnoreNullStrings(true); - config.setFetchTo(FetchTo.DATA); + config.setFetchTo(TbMsgSource.DATA); node.config = config; - node.fetchTo = FetchTo.DATA; + node.fetchTo = TbMsgSource.DATA; var msgMetaData = new TbMsgMetaData(); var msgData = "{\"temp\":42,\"humidity\":77}"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); + + when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); + when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID); + when(deviceServiceMock.findDeviceById(eq(DUMMY_TENANT_ID), eq(device.getId()))).thenReturn(device); + + when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); + verify(ctxMock, never()).tellFailure(any(), any()); + + var expectedMsgData = "{\"temp\":42,\"humidity\":77,\"originatorName\":\"Test device\",\"originatorType\":\"Test device type\"}"; + + assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData); + assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msgMetaData); + } + + @Test + public void givenDeviceWithEmptyLabel_whenOnMsg_thenShouldTellSuccessAndFetchToData() throws TbNodeException, ExecutionException, InterruptedException { + // GIVEN + var device = new Device(); + device.setId(DUMMY_DEVICE_ORIGINATOR); + device.setName("Test device"); + device.setType("Test device type"); + device.setLabel(""); + + config.setDataMapping(Map.of( + "name", "originatorName", + "type", "originatorType", + "label", "originatorLabel")); + config.setIgnoreNullStrings(true); + config.setFetchTo(TbMsgSource.DATA); + + node.config = config; + node.fetchTo = TbMsgSource.DATA; + var msgMetaData = new TbMsgMetaData(); + var msgData = "{\"temp\":42,\"humidity\":77}"; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID); @@ -213,15 +240,15 @@ public class TbGetOriginatorFieldsNodeTest { "type", "originatorType", "label", "originatorLabel")); config.setIgnoreNullStrings(true); - config.setFetchTo(FetchTo.METADATA); + config.setFetchTo(TbMsgSource.METADATA); node.config = config; - node.fetchTo = FetchTo.METADATA; + node.fetchTo = TbMsgSource.METADATA; var msgMetaData = new TbMsgMetaData(Map.of( "testKey1", "testValue1", "testKey2", "123")); var msgData = "[\"value1\",\"value2\"]"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID); @@ -261,15 +288,15 @@ public class TbGetOriginatorFieldsNodeTest { "type", "originatorType", "label", "originatorLabel")); config.setIgnoreNullStrings(false); - config.setFetchTo(FetchTo.METADATA); + config.setFetchTo(TbMsgSource.METADATA); node.config = config; - node.fetchTo = FetchTo.METADATA; + node.fetchTo = TbMsgSource.METADATA; var msgMetaData = new TbMsgMetaData(Map.of( "testKey1", "testValue1", "testKey2", "123")); var msgData = "[\"value1\",\"value2\"]"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID); @@ -319,15 +346,15 @@ public class TbGetOriginatorFieldsNodeTest { "type", "originatorType", "label", "originatorLabel")); config.setIgnoreNullStrings(false); - config.setFetchTo(FetchTo.METADATA); + config.setFetchTo(TbMsgSource.METADATA); node.config = config; - node.fetchTo = FetchTo.METADATA; + node.fetchTo = TbMsgSource.METADATA; var msgMetaData = new TbMsgMetaData(Map.of( "testKey1", "testValue1", "testKey2", "123")); var msgData = "[\"value1\",\"value2\"]"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", new DashboardId(UUID.randomUUID()), msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, new DashboardId(UUID.randomUUID()), msgMetaData, msgData); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java index 23ceb082bc..dcf5c22ea5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java @@ -17,9 +17,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,12 +28,15 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.data.RelationsQuery; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; @@ -54,13 +55,13 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.relation.RelationService; @@ -72,7 +73,6 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.UUID; -import java.util.concurrent.Callable; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -86,28 +86,13 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; @ExtendWith(MockitoExtension.class) public class TbGetRelatedAttributeNodeTest { private static final EntityId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -172,7 +157,7 @@ public class TbGetRelatedAttributeNodeTest { assertThat(nodeConfig).isEqualTo(config); assertThat(nodeConfig.getDataMapping()).isEqualTo(Map.of("serialNumber", "sn")); assertThat(nodeConfig.getDataToFetch()).isEqualTo(DataToFetch.ATTRIBUTES); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); var relationsQuery = new RelationsQuery(); var relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); @@ -191,7 +176,7 @@ public class TbGetRelatedAttributeNodeTest { "sourceAttr2", "targetKey2", "sourceAttr3", "targetKey3")); config.setDataToFetch(DataToFetch.LATEST_TELEMETRY); - config.setFetchTo(FetchTo.DATA); + config.setFetchTo(TbMsgSource.DATA); var relationsQuery = new RelationsQuery(); var relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); @@ -214,7 +199,7 @@ public class TbGetRelatedAttributeNodeTest { "sourceAttr3", "targetKey3" )); assertThat(nodeConfig.getDataToFetch()).isEqualTo(DataToFetch.LATEST_TELEMETRY); - assertThat(node.fetchTo).isEqualTo(FetchTo.DATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.DATA); assertThat(nodeConfig.getRelationsQuery()).isEqualTo(relationsQuery); } @@ -237,8 +222,8 @@ public class TbGetRelatedAttributeNodeTest { @Test public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { // GIVEN - node.fetchTo = FetchTo.DATA; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); + node.fetchTo = TbMsgSource.DATA; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -251,7 +236,7 @@ public class TbGetRelatedAttributeNodeTest { @Test public void givenDidNotFindEntity_whenOnMsg_thenShouldTellFailure() { // GIVEN - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.ATTRIBUTES, DUMMY_DEVICE_ORIGINATOR); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.ATTRIBUTES, DUMMY_DEVICE_ORIGINATOR); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -287,7 +272,7 @@ public class TbGetRelatedAttributeNodeTest { var customer = new Customer(new CustomerId(UUID.randomUUID())); var user = new User(new UserId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.ATTRIBUTES, customer.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.ATTRIBUTES, customer.getId()); entityRelation.setFrom(customer.getId()); entityRelation.setTo(user.getId()); @@ -306,7 +291,7 @@ public class TbGetRelatedAttributeNodeTest { doReturn(Futures.immediateFuture(List.of(entityRelation))).when(relationServiceMock).findByQuery(eq(TENANT_ID), any()); when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); - when(attributesServiceMock.find(eq(TENANT_ID), eq(user.getId()), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + when(attributesServiceMock.find(eq(TENANT_ID), eq(user.getId()), eq(DataConstants.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) .thenReturn(Futures.immediateFuture(attributes)); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); @@ -338,7 +323,7 @@ public class TbGetRelatedAttributeNodeTest { var firstCustomer = new Customer(new CustomerId(UUID.randomUUID())); var secondCustomer = new Customer(new CustomerId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.ATTRIBUTES, firstCustomer.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.ATTRIBUTES, firstCustomer.getId()); entityRelation.setFrom(firstCustomer.getId()); entityRelation.setTo(secondCustomer.getId()); @@ -357,7 +342,7 @@ public class TbGetRelatedAttributeNodeTest { doReturn(Futures.immediateFuture(List.of(entityRelation))).when(relationServiceMock).findByQuery(eq(TENANT_ID), any()); when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); - when(attributesServiceMock.find(eq(TENANT_ID), eq(secondCustomer.getId()), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + when(attributesServiceMock.find(eq(TENANT_ID), eq(secondCustomer.getId()), eq(DataConstants.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) .thenReturn(Futures.immediateFuture(attributes)); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); @@ -389,7 +374,7 @@ public class TbGetRelatedAttributeNodeTest { var dashboard = new Dashboard(new DashboardId(UUID.randomUUID())); var entityView = new EntityView(new EntityViewId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.LATEST_TELEMETRY, dashboard.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.LATEST_TELEMETRY, dashboard.getId()); entityRelation.setFrom(dashboard.getId()); entityRelation.setTo(entityView.getId()); @@ -440,7 +425,7 @@ public class TbGetRelatedAttributeNodeTest { var tenant = new Tenant(new TenantId(UUID.randomUUID())); var device = new Device(new DeviceId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.LATEST_TELEMETRY, tenant.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.LATEST_TELEMETRY, tenant.getId()); entityRelation.setFrom(tenant.getId()); entityRelation.setTo(device.getId()); @@ -493,7 +478,7 @@ public class TbGetRelatedAttributeNodeTest { device.setName("Device Name"); var asset = new Asset(new AssetId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.FIELDS, asset.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.FIELDS, asset.getId()); entityRelation.setFrom(asset.getId()); entityRelation.setTo(device.getId()); @@ -533,7 +518,7 @@ public class TbGetRelatedAttributeNodeTest { device.setName("Device Name"); var asset = new Asset(new AssetId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.FIELDS, asset.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.FIELDS, asset.getId()); entityRelation.setFrom(asset.getId()); entityRelation.setTo(device.getId()); @@ -583,7 +568,7 @@ public class TbGetRelatedAttributeNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } - private void prepareMsgAndConfig(FetchTo fetchTo, DataToFetch dataToFetch, EntityId originator) { + private void prepareMsgAndConfig(TbMsgSource fetchTo, DataToFetch dataToFetch, EntityId originator) { config.setDataToFetch(dataToFetch); config.setFetchTo(fetchTo); @@ -607,7 +592,7 @@ public class TbGetRelatedAttributeNodeTest { msgData = "{\"temp\":42,\"humidity\":77,\"messageBodyPattern1\":\"targetKey2\",\"messageBodyPattern2\":\"sourceKey3\"}"; } - msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), originator, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, msgMetaData, msgData); } @RequiredArgsConstructor diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java index d8532247ae..87b34cffe0 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java @@ -17,9 +17,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,10 +27,12 @@ import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.util.TbMsgSource; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -53,7 +54,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.concurrent.Callable; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -64,28 +64,13 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; @ExtendWith(MockitoExtension.class) public class TbGetTenantAttributeNodeTest { private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -157,7 +142,7 @@ public class TbGetTenantAttributeNodeTest { assertThat(node.config).isEqualTo(config); assertThat(config.getDataMapping()).isEqualTo(Map.of("alarmThreshold", "threshold")); assertThat(config.getDataToFetch()).isEqualTo(DataToFetch.ATTRIBUTES); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); } @Test @@ -168,7 +153,7 @@ public class TbGetTenantAttributeNodeTest { "sourceAttr2", "targetKey2", "sourceAttr3", "targetKey3")); config.setDataToFetch(DataToFetch.LATEST_TELEMETRY); - config.setFetchTo(FetchTo.DATA); + config.setFetchTo(TbMsgSource.DATA); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); // WHEN @@ -181,7 +166,7 @@ public class TbGetTenantAttributeNodeTest { "sourceAttr2", "targetKey2", "sourceAttr3", "targetKey3")); assertThat(config.getDataToFetch()).isEqualTo(DataToFetch.LATEST_TELEMETRY); - assertThat(node.fetchTo).isEqualTo(FetchTo.DATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.DATA); } @Test @@ -203,8 +188,8 @@ public class TbGetTenantAttributeNodeTest { @Test public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { // GIVEN - node.fetchTo = FetchTo.DATA; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); + node.fetchTo = TbMsgSource.DATA; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -219,7 +204,7 @@ public class TbGetTenantAttributeNodeTest { // GIVEN var deviceId = new DeviceId(UUID.randomUUID()); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.ATTRIBUTES, deviceId); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.ATTRIBUTES, deviceId); List attributesList = List.of( new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L), @@ -231,7 +216,7 @@ public class TbGetTenantAttributeNodeTest { when(ctxMock.getTenantId()).thenReturn(TENANT_ID); when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); - when(attributesServiceMock.find(eq(TENANT_ID), eq(TENANT_ID), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + when(attributesServiceMock.find(eq(TENANT_ID), eq(TENANT_ID), eq(DataConstants.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) .thenReturn(Futures.immediateFuture(attributesList)); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); @@ -260,7 +245,7 @@ public class TbGetTenantAttributeNodeTest { @Test public void givenFetchAttributesToMetaData_whenOnMsg_thenShouldFetchAttributesToMetaData() { // GIVEN - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.ATTRIBUTES, TENANT_ID); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.ATTRIBUTES, TENANT_ID); List attributesList = List.of( new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L), @@ -272,7 +257,7 @@ public class TbGetTenantAttributeNodeTest { when(ctxMock.getTenantId()).thenReturn(TENANT_ID); when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); - when(attributesServiceMock.find(eq(TENANT_ID), eq(TENANT_ID), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + when(attributesServiceMock.find(eq(TENANT_ID), eq(TENANT_ID), eq(DataConstants.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) .thenReturn(Futures.immediateFuture(attributesList)); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); @@ -303,7 +288,7 @@ public class TbGetTenantAttributeNodeTest { // GIVEN var customerId = new CustomerId(UUID.randomUUID()); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.LATEST_TELEMETRY, customerId); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.LATEST_TELEMETRY, customerId); List timeseries = List.of( new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")), @@ -346,7 +331,7 @@ public class TbGetTenantAttributeNodeTest { // GIVEN var ruleChainId = new RuleChainId(UUID.randomUUID()); - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.LATEST_TELEMETRY, ruleChainId); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.LATEST_TELEMETRY, ruleChainId); List timeseries = List.of( new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")), @@ -395,7 +380,7 @@ public class TbGetTenantAttributeNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } - private void prepareMsgAndConfig(FetchTo fetchTo, DataToFetch dataToFetch, EntityId originator) { + private void prepareMsgAndConfig(TbMsgSource fetchTo, DataToFetch dataToFetch, EntityId originator) { config.setDataMapping(Map.of( "sourceKey1", "targetKey1", "${metaDataPattern1}", "$[messageBodyPattern1]", @@ -412,7 +397,7 @@ public class TbGetTenantAttributeNodeTest { var msgData = "{\"temp\":42,\"humidity\":77,\"messageBodyPattern1\":\"targetKey2\",\"messageBodyPattern2\":\"sourceKey3\"}"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", originator, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, msgMetaData, msgData); } @RequiredArgsConstructor diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java index 6e1e619eab..bfbaa9a92e 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java @@ -29,9 +29,11 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.util.ContactBasedEntityDetails; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -102,14 +104,14 @@ public class TbGetTenantDetailsNodeTest { public void givenDefaultConfig_whenInit_thenOK() { // THEN assertThat(config.getDetailsList()).isEqualTo(Collections.emptyList()); - assertThat(config.getFetchTo()).isEqualTo(FetchTo.DATA); + assertThat(config.getFetchTo()).isEqualTo(TbMsgSource.DATA); } @Test public void givenCustomConfig_whenInit_thenOK() throws TbNodeException { // GIVEN config.setDetailsList(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE)); - config.setFetchTo(FetchTo.METADATA); + config.setFetchTo(TbMsgSource.METADATA); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); // WHEN @@ -118,15 +120,15 @@ public class TbGetTenantDetailsNodeTest { // THEN assertThat(node.config).isEqualTo(config); assertThat(config.getDetailsList()).isEqualTo(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE)); - assertThat(config.getFetchTo()).isEqualTo(FetchTo.METADATA); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(config.getFetchTo()).isEqualTo(TbMsgSource.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); } @Test public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { // GIVEN - node.fetchTo = FetchTo.DATA; - msg = TbMsg.newMsg("SOME_MESSAGE_TYPE", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); + node.fetchTo = TbMsgSource.DATA; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -139,7 +141,7 @@ public class TbGetTenantDetailsNodeTest { @Test public void givenAllEntityDetailsAndFetchToData_whenOnMsg_thenShouldTellSuccessAndFetchAllToData() { // GIVEN - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.values())); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.values())); mockFindTenant(); @@ -172,7 +174,7 @@ public class TbGetTenantDetailsNodeTest { @Test public void givenSomeEntityDetailsAndFetchToMetadata_whenOnMsg_thenShouldTellSuccessAndFetchSomeToMetaData() { // GIVEN - prepareMsgAndConfig(FetchTo.METADATA, List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.TITLE, ContactBasedEntityDetails.PHONE)); + prepareMsgAndConfig(TbMsgSource.METADATA, List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.TITLE, ContactBasedEntityDetails.PHONE)); mockFindTenant(); @@ -201,7 +203,7 @@ public class TbGetTenantDetailsNodeTest { tenant.setAddress(null); tenant.setAddress2(null); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2)); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2)); mockFindTenant(); @@ -221,7 +223,7 @@ public class TbGetTenantDetailsNodeTest { @Test public void givenDidNotFindTenant_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() { // GIVEN - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2)); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2)); when(ctxMock.getTenantId()).thenReturn(tenant.getId()); when(ctxMock.getTenantService()).thenReturn(tenantServiceMock); @@ -245,7 +247,7 @@ public class TbGetTenantDetailsNodeTest { // GIVEN tenant.setAdditionalInfo(JacksonUtil.toJsonNode("{\"someProperty\":\"someValue\",\"description\":null}")); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ADDITIONAL_INFO)); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ADDITIONAL_INFO)); mockFindTenant(); @@ -273,7 +275,7 @@ public class TbGetTenantDetailsNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } - private void prepareMsgAndConfig(FetchTo fetchTo, List detailsList) { + private void prepareMsgAndConfig(TbMsgSource fetchTo, List detailsList) { config.setDetailsList(detailsList); config.setFetchTo(fetchTo); @@ -286,7 +288,7 @@ public class TbGetTenantDetailsNodeTest { var msgData = "{\"dataKey1\":123,\"dataKey2\":\"dataValue2\"}"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); } private void mockFindTenant() { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java index d33b2af030..eef77a5304 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java @@ -22,7 +22,6 @@ import org.mockito.ArgumentCaptor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; @@ -39,12 +38,12 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; @@ -92,9 +91,10 @@ public class DeviceStateTest { }); when(ctx.getAlarmService()).thenReturn(alarmService); - when(ctx.newMsg(any(), any(), any(), any(), any(), any())).thenAnswer(invocationOnMock -> { + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), any())).thenAnswer(invocationOnMock -> { + TbMsgType type = invocationOnMock.getArgument(1); String data = invocationOnMock.getArgument(invocationOnMock.getArguments().length - 1); - return TbMsg.newMsg(null, null, new TbMsgMetaData(), data); + return TbMsg.newMsg(type, null, TbMsgMetaData.EMPTY, data); }); } @@ -106,8 +106,8 @@ public class DeviceStateTest { DeviceId deviceId = new DeviceId(UUID.randomUUID()); DeviceState deviceState = createDeviceState(deviceId, alarmConfig); - TbMsg attributeUpdateMsg = TbMsg.newMsg(SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), - deviceId, new TbMsgMetaData(), "{ \"enabled\": false }"); + TbMsg attributeUpdateMsg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, + deviceId, TbMsgMetaData.EMPTY, "{ \"enabled\": false }"); deviceState.process(ctx, attributeUpdateMsg); @@ -115,11 +115,11 @@ public class DeviceStateTest { verify(ctx).enqueueForTellNext(resultMsgCaptor.capture(), eq("Alarm Created")); Alarm alarm = JacksonUtil.fromString(resultMsgCaptor.getValue().getData(), Alarm.class); - deviceState.process(ctx, TbMsg.newMsg(DataConstants.ALARM_CLEAR, deviceId, new TbMsgMetaData(), JacksonUtil.toString(alarm))); + deviceState.process(ctx, TbMsg.newMsg(TbMsgType.ALARM_CLEAR, deviceId, TbMsgMetaData.EMPTY, JacksonUtil.toString(alarm))); reset(ctx); String deletedAttributes = "{ \"attributes\": [ \"other\" ] }"; - deviceState.process(ctx, TbMsg.newMsg(DataConstants.ATTRIBUTES_DELETED, deviceId, new TbMsgMetaData(), deletedAttributes)); + deviceState.process(ctx, TbMsg.newMsg(TbMsgType.ATTRIBUTES_DELETED, deviceId, TbMsgMetaData.EMPTY, deletedAttributes)); verify(ctx, never()).enqueueForTellNext(any(), anyString()); } @@ -129,17 +129,17 @@ public class DeviceStateTest { DeviceId deviceId = new DeviceId(UUID.randomUUID()); DeviceState deviceState = createDeviceState(deviceId, alarmConfig); - TbMsg attributeUpdateMsg = TbMsg.newMsg(SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), - deviceId, new TbMsgMetaData(), "{ \"enabled\": false }"); + TbMsg attributeUpdateMsg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, + deviceId, TbMsgMetaData.EMPTY, "{ \"enabled\": false }"); deviceState.process(ctx, attributeUpdateMsg); ArgumentCaptor resultMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx).enqueueForTellNext(resultMsgCaptor.capture(), eq("Alarm Created")); Alarm alarm = JacksonUtil.fromString(resultMsgCaptor.getValue().getData(), Alarm.class); - deviceState.process(ctx, TbMsg.newMsg(DataConstants.ALARM_CLEAR, deviceId, new TbMsgMetaData(), JacksonUtil.toString(alarm))); + deviceState.process(ctx, TbMsg.newMsg(TbMsgType.ALARM_CLEAR, deviceId, TbMsgMetaData.EMPTY, JacksonUtil.toString(alarm))); - TbMsg alarmDeleteNotification = TbMsg.newMsg(DataConstants.ALARM_DELETE, deviceId, new TbMsgMetaData(), JacksonUtil.toString(alarm)); + TbMsg alarmDeleteNotification = TbMsg.newMsg(TbMsgType.ALARM_DELETE, deviceId, TbMsgMetaData.EMPTY, JacksonUtil.toString(alarm)); assertDoesNotThrow(() -> { deviceState.process(ctx, alarmDeleteNotification); }); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java index 80ed769a71..03a9c17a60 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java @@ -54,6 +54,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValueSourceType; @@ -64,7 +65,6 @@ import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey; @@ -122,8 +122,8 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 42); - TbMsg msg = TbMsg.newMsg("123456789", deviceId, new TbMsgMetaData(), - TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); + TbMsg msg = TbMsg.newMsg("123456789", deviceId, TbMsgMetaData.EMPTY, + TbMsgDataType.JSON, JacksonUtil.toString(data)); node.onMsg(ctx, msg); verify(ctx).tellSuccess(msg); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); @@ -141,7 +141,7 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 42); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); verify(ctx).tellSuccess(msg); @@ -193,25 +193,25 @@ public class TbDeviceProfileNodeTest { Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg); + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())).thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 42); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); verify(ctx).tellSuccess(msg); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); - TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg2); + TbMsg theMsg2 = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, "2"); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())).thenReturn(theMsg2); registerCreateAlarmMock(alarmService.updateAlarm(any()), false); - TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg2); verify(ctx).tellSuccess(msg2); @@ -286,13 +286,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(attrListListenableFuture); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.any(TbMsgType.class), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 21); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -373,13 +373,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.anyString(), Mockito.anyString())) .thenReturn(attrListListenableFuture); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 21); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -442,13 +442,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 35); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -536,13 +536,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 35); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -557,7 +557,7 @@ public class TbDeviceProfileNodeTest { Thread.sleep(halfOfAlarmDelay); - TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg2); @@ -660,13 +660,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listNoDurationAttribute); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 150); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -681,7 +681,7 @@ public class TbDeviceProfileNodeTest { Thread.sleep(halfOfAlarmDelay); - TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg2); @@ -769,13 +769,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 150); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -784,7 +784,7 @@ public class TbDeviceProfileNodeTest { verify(ctx, Mockito.never()).tellNext(theMsg, "Alarm Created"); data.put("temperature", 151); - TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg2); @@ -885,13 +885,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listNoDurationAttribute); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 150); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -900,7 +900,7 @@ public class TbDeviceProfileNodeTest { verify(ctx, Mockito.never()).tellNext(theMsg, "Alarm Created"); data.put("temperature", 151); - TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg2); @@ -981,13 +981,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 35); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -1002,7 +1002,7 @@ public class TbDeviceProfileNodeTest { Thread.sleep(halfOfAlarmDelay); - TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg2); @@ -1079,13 +1079,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 35); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -1161,13 +1161,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureActiveSchedule); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 35); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); // Mockito.reset(ctx); @@ -1257,11 +1257,11 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureInactiveSchedule); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 35); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -1335,13 +1335,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(customerId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 25); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -1408,13 +1408,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 40); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -1491,13 +1491,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 150L); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -1576,13 +1576,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 150L); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java index 7b6a54ae0b..48aca3b573 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java @@ -32,6 +32,7 @@ import org.springframework.web.client.AsyncRestTemplate; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -144,16 +145,15 @@ public class TbHttpClientTest { var httpClient = new TbHttpClient(config, eventLoop); httpClient.setHttpClient(asyncRestTemplate); - var msg = TbMsg.newMsg("GET", new DeviceId(EntityId.NULL_UUID), TbMsgMetaData.EMPTY, "{}"); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, new DeviceId(EntityId.NULL_UUID), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); var successMsg = TbMsg.newMsg( - "SUCCESS", msg.getOriginator(), + TbMsgType.POST_TELEMETRY_REQUEST, msg.getOriginator(), msg.getMetaData(), msg.getData() ); var ctx = mock(TbContext.class); when(ctx.transformMsg( - eq(msg), eq(msg.getType()), - eq(msg.getOriginator()), + eq(msg), eq(msg.getMetaData()), eq(msg.getData()) )).thenReturn(successMsg); @@ -161,15 +161,14 @@ public class TbHttpClientTest { var capturedData = ArgumentCaptor.forClass(String.class); when(ctx.transformMsg( - eq(msg), eq(msg.getType()), - eq(msg.getOriginator()), + eq(msg), any(), capturedData.capture() )).thenReturn(successMsg); httpClient.processMessage(ctx, msg, m -> ctx.tellSuccess(msg), - (m, t) -> ctx.tellFailure(m, t)); + ctx::tellFailure); Awaitility.await() .atMost(30, TimeUnit.SECONDS) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java index 9185a31118..705f298513 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.rest; import com.datastax.oss.driver.api.core.uuid.Uuids; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.HttpException; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; @@ -39,6 +38,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -136,23 +136,18 @@ public class TbRestApiCallNodeTest { config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); initWithConfig(config); - TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); restNode.onMsg(ctx, msg); assertTrue("Server handled request", latch.await(10, TimeUnit.SECONDS)); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + verify(ctx).transformMsg(msgCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - - assertEquals("USER", typeCaptor.getValue()); - assertEquals(originator, originatorCaptor.getValue()); assertNotSame(metaData, metadataCaptor.getValue()); - assertEquals("{}", dataCaptor.getValue()); + assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue()); } @Test @@ -202,22 +197,18 @@ public class TbRestApiCallNodeTest { config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); initWithConfig(config); - TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); restNode.onMsg(ctx, msg); assertTrue("Server handled request", latch.await(10, TimeUnit.SECONDS)); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + verify(ctx).transformMsg(msgCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("USER", typeCaptor.getValue()); - assertEquals(originator, originatorCaptor.getValue()); assertNotSame(metaData, metadataCaptor.getValue()); - assertEquals("{}", dataCaptor.getValue()); + assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue()); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java index 27faa8313a..7b12dbb2b1 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java @@ -31,10 +31,10 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.edge.EdgeEventService; import java.util.UUID; @@ -80,7 +80,7 @@ public class TbSendRPCReplyNodeTest { Mockito.when(ctx.getRpcService()).thenReturn(rpcService); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, getDefaultMetadata(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, getDefaultMetadata(), TbMsgDataType.JSON, DUMMY_DATA, null, null); node.onMsg(ctx, msg); @@ -99,7 +99,7 @@ public class TbSendRPCReplyNodeTest { TbMsgMetaData defaultMetadata = getDefaultMetadata(); defaultMetadata.putValue(DataConstants.EDGE_ID, UUID.randomUUID().toString()); defaultMetadata.putValue(DataConstants.DEVICE_ID, UUID.randomUUID().toString()); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, defaultMetadata, + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, defaultMetadata, TbMsgDataType.JSON, DUMMY_DATA, null, null); node.onMsg(ctx, msg); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfigurationTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfigurationTest.java new file mode 100644 index 0000000000..2bc2c0ba0e --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfigurationTest.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 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.rule.engine.telemetry; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TbMsgAttributesNodeConfigurationTest { + + @Test + void testDefaultConfig_givenupdateAttributesOnlyOnValueChange_thenTrue_sinceVersion1() { + assertThat(new TbMsgAttributesNodeConfiguration().defaultConfiguration().isUpdateAttributesOnlyOnValueChange()).isTrue(); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java new file mode 100644 index 0000000000..818c1bb97f --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java @@ -0,0 +1,143 @@ +/** + * Copyright © 2016-2023 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.rule.engine.telemetry; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.util.TbPair; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.BDDMockito.willCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +@Slf4j +class TbMsgAttributesNodeTest { + + final String updateAttributesOnlyOnValueChangeKey = "updateAttributesOnlyOnValueChange"; + + @Test + void testFilterChangedAttr_whenCurrentAttributesEmpty_thenReturnNewAttributes() { + TbMsgAttributesNode node = spy(TbMsgAttributesNode.class); + List newAttributes = new ArrayList<>(); + + List filtered = node.filterChangedAttr(Collections.emptyList(), newAttributes); + assertThat(filtered).isSameAs(newAttributes); + } + + @Test + void testFilterChangedAttr_whenCurrentAttributesContainsInAnyOrderNewAttributes_thenReturnEmptyList() { + TbMsgAttributesNode node = spy(TbMsgAttributesNode.class); + List currentAttributes = List.of( + new BaseAttributeKvEntry(1694000000L, new StringDataEntry("address", "Peremohy ave 1")), + new BaseAttributeKvEntry(1694000000L, new BooleanDataEntry("valid", true)), + new BaseAttributeKvEntry(1694000000L, new LongDataEntry("counter", 100L)), + new BaseAttributeKvEntry(1694000000L, new DoubleDataEntry("temp", -18.35)), + new BaseAttributeKvEntry(1694000000L, new JsonDataEntry("json", "{\"warning\":\"out of paper\"}")) + ); + List newAttributes = new ArrayList<>(currentAttributes); + newAttributes.add(newAttributes.get(0)); + newAttributes.remove(0); + assertThat(newAttributes).hasSize(currentAttributes.size()); + assertThat(currentAttributes).isNotEmpty(); + assertThat(newAttributes).containsExactlyInAnyOrderElementsOf(currentAttributes); + + List filtered = node.filterChangedAttr(currentAttributes, newAttributes); + assertThat(filtered).isEmpty(); //no changes + } + + @Test + void testFilterChangedAttr_whenCurrentAttributesContainsInAnyOrderNewAttributes_thenReturnExpectedList() { + TbMsgAttributesNode node = spy(TbMsgAttributesNode.class); + List currentAttributes = List.of( + new BaseAttributeKvEntry(1694000000L, new StringDataEntry("address", "Peremohy ave 1")), + new BaseAttributeKvEntry(1694000000L, new BooleanDataEntry("valid", true)), + new BaseAttributeKvEntry(1694000000L, new LongDataEntry("counter", 100L)), + new BaseAttributeKvEntry(1694000000L, new DoubleDataEntry("temp", -18.35)), + new BaseAttributeKvEntry(1694000000L, new JsonDataEntry("json", "{\"warning\":\"out of paper\"}")) + ); + List newAttributes = List.of( + new BaseAttributeKvEntry(1694000999L, new JsonDataEntry("json", "{\"status\":\"OK\"}")), // value changed, reordered + new BaseAttributeKvEntry(1694000999L, new StringDataEntry("valid", "true")), //type changed + new BaseAttributeKvEntry(1694000999L, new LongDataEntry("counter", 101L)), //value changed + new BaseAttributeKvEntry(1694000999L, new DoubleDataEntry("temp", -18.35)), + new BaseAttributeKvEntry(1694000999L, new StringDataEntry("address", "Peremohy ave 1")) // reordered + ); + List expected = List.of( + new BaseAttributeKvEntry(1694000999L, new StringDataEntry("valid", "true")), + new BaseAttributeKvEntry(1694000999L, new LongDataEntry("counter", 101L)), + new BaseAttributeKvEntry(1694000999L, new JsonDataEntry("json", "{\"status\":\"OK\"}")) + ); + + List filtered = node.filterChangedAttr(currentAttributes, newAttributes); + assertThat(filtered).containsExactlyInAnyOrderElementsOf(expected); + } + + @Test + void testUpgrade_fromVersion0() throws TbNodeException { + + TbMsgAttributesNode node = mock(TbMsgAttributesNode.class); + willCallRealMethod().given(node).upgrade(anyInt(), any()); + + ObjectNode jsonNode = (ObjectNode) JacksonUtil.valueToTree(new TbMsgAttributesNodeConfiguration().defaultConfiguration()); + jsonNode.remove(updateAttributesOnlyOnValueChangeKey); + assertThat(jsonNode.has(updateAttributesOnlyOnValueChangeKey)).as("pre condition has no " + updateAttributesOnlyOnValueChangeKey).isFalse(); + + TbPair upgradeResult = node.upgrade(0, jsonNode); + + ObjectNode resultNode = (ObjectNode) upgradeResult.getSecond(); + assertThat(upgradeResult.getFirst()).as("upgrade result has changes").isTrue(); + assertThat(resultNode.has(updateAttributesOnlyOnValueChangeKey)).as("upgrade result has key " + updateAttributesOnlyOnValueChangeKey).isTrue(); + assertThat(resultNode.get(updateAttributesOnlyOnValueChangeKey).asBoolean()).as("upgrade result value [false] for key " + updateAttributesOnlyOnValueChangeKey).isFalse(); + } + + @Test + void testUpgrade_fromVersion0_alreadyHasupdateAttributesOnlyOnValueChange() throws TbNodeException { + TbMsgAttributesNode node = mock(TbMsgAttributesNode.class); + willCallRealMethod().given(node).upgrade(anyInt(), any()); + + ObjectNode jsonNode = (ObjectNode) JacksonUtil.valueToTree(new TbMsgAttributesNodeConfiguration().defaultConfiguration()); + jsonNode.remove(updateAttributesOnlyOnValueChangeKey); + jsonNode.put(updateAttributesOnlyOnValueChangeKey, true); + assertThat(jsonNode.has(updateAttributesOnlyOnValueChangeKey)).as("pre condition has no " + updateAttributesOnlyOnValueChangeKey).isTrue(); + assertThat(jsonNode.get(updateAttributesOnlyOnValueChangeKey).asBoolean()).as("pre condition has [true] for key " + updateAttributesOnlyOnValueChangeKey).isTrue(); + + TbPair upgradeResult = node.upgrade(0, jsonNode); + + ObjectNode resultNode = (ObjectNode) upgradeResult.getSecond(); + assertThat(upgradeResult.getFirst()).as("upgrade result has changes").isFalse(); + assertThat(resultNode.has(updateAttributesOnlyOnValueChangeKey)).as("upgrade result has key " + updateAttributesOnlyOnValueChangeKey).isTrue(); + assertThat(resultNode.get(updateAttributesOnlyOnValueChangeKey).asBoolean()).as("upgrade result value [true] for key " + updateAttributesOnlyOnValueChangeKey).isTrue(); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNodeTest.java index c0839f5b61..632243676f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNodeTest.java @@ -25,7 +25,9 @@ import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -49,10 +51,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY; -import static org.thingsboard.server.common.data.DataConstants.SCOPE; -import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; -import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; @Slf4j public class TbMsgDeleteAttributesNodeTest { @@ -94,7 +92,7 @@ public class TbMsgDeleteAttributesNodeTest { @Test void givenDefaultConfig_whenVerify_thenOK() { TbMsgDeleteAttributesNodeConfiguration defaultConfig = new TbMsgDeleteAttributesNodeConfiguration().defaultConfiguration(); - assertThat(defaultConfig.getScope()).isEqualTo(SERVER_SCOPE); + assertThat(defaultConfig.getScope()).isEqualTo(DataConstants.SERVER_SCOPE); assertThat(defaultConfig.getKeys()).isEqualTo(Collections.emptyList()); } @@ -115,7 +113,7 @@ public class TbMsgDeleteAttributesNodeTest { void givenMsg_whenOnMsg_thenVerifyOutput_SendAttributesDeletedNotification_NotifyDevice() throws Exception { config.setSendAttributesDeletedNotification(true); config.setNotifyDevice(true); - config.setScope(SHARED_SCOPE); + config.setScope(DataConstants.SHARED_SCOPE); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctx, nodeConfiguration); onMsg_thenVerifyOutput(true, true, false); @@ -135,12 +133,12 @@ public class TbMsgDeleteAttributesNodeTest { ); TbMsgMetaData metaData = new TbMsgMetaData(mdMap); if (notifyDeviceMetadata) { - metaData.putValue(NOTIFY_DEVICE_METADATA_KEY, "true"); - metaData.putValue(SCOPE, SHARED_SCOPE); + metaData.putValue(DataConstants.NOTIFY_DEVICE_METADATA_KEY, "true"); + metaData.putValue(DataConstants.SCOPE, DataConstants.SHARED_SCOPE); } final String data = "{\"TestAttribute_2\": \"humidity\", \"TestAttribute_3\": \"voltage\"}"; - TbMsg msg = TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", deviceId, metaData, data, callback); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, deviceId, metaData, data, callback); node.onMsg(ctx, msg); ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java index f4cd097878..8ea7806ae2 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.transform; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,6 +25,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; @@ -35,13 +35,13 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.asset.AssetService; import java.util.NoSuchElementException; -import java.util.concurrent.Callable; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -66,21 +66,7 @@ public class TbChangeOriginatorNodeTest { @Before public void before() { - dbExecutor = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(Runnable command) { - command.run(); - } - }; + dbExecutor = new TestDbCallbackExecutor(); } @Test @@ -94,7 +80,7 @@ public class TbChangeOriginatorNodeTest { RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); - TbMsg msg = TbMsg.newMsg( "ASSET", assetId, new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, assetId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); when(ctx.getAssetService()).thenReturn(assetService); when(assetService.findAssetByIdAsync(any(),eq( assetId))).thenReturn(Futures.immediateFuture(asset)); @@ -102,11 +88,8 @@ public class TbChangeOriginatorNodeTest { node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + verify(ctx).transformMsgOriginator(msgCaptor.capture(), originatorCaptor.capture()); assertEquals(customerId, originatorCaptor.getValue()); } @@ -122,18 +105,15 @@ public class TbChangeOriginatorNodeTest { RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); - TbMsg msg = TbMsg.newMsg( "ASSET", assetId, new TbMsgMetaData(), TbMsgDataType.JSON,"{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, assetId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON,TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); when(ctx.getAssetService()).thenReturn(assetService); when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(asset)); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + verify(ctx).transformMsgOriginator(msgCaptor.capture(), originatorCaptor.capture()); assertEquals(customerId, originatorCaptor.getValue()); } @@ -149,7 +129,7 @@ public class TbChangeOriginatorNodeTest { RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); - TbMsg msg = TbMsg.newMsg( "ASSET", assetId, new TbMsgMetaData(), TbMsgDataType.JSON,"{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, assetId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON,TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); when(ctx.getAssetService()).thenReturn(assetService); when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(null)); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java index 2666cae477..e0ab34e35b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java @@ -26,6 +26,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -78,8 +79,7 @@ public class TbCopyKeysNodeTest { @Test void givenMsgFromMetadata_whenOnMsg_thenVerifyOutput() throws Exception { - String data = "{}"; - node.onMsg(ctx, getTbMsg(deviceId, data)); + node.onMsg(ctx, getTbMsg(deviceId, TbMsg.EMPTY_JSON_OBJECT)); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx, times(1)).tellSuccess(newMsgCaptor.capture()); @@ -136,8 +136,7 @@ public class TbCopyKeysNodeTest { @Test void givenMsgDataNotJSONObject_whenOnMsg_thenTVerifyOutput() throws Exception { - String data = "[]"; - TbMsg msg = getTbMsg(deviceId, data); + TbMsg msg = getTbMsg(deviceId, TbMsg.EMPTY_JSON_ARRAY); node.onMsg(ctx, msg); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); @@ -157,7 +156,7 @@ public class TbCopyKeysNodeTest { "voltageDataValue", "220", "city", "NY" ); - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(mdMap), data, callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, new TbMsgMetaData(mdMap), data, callback); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java index 7152c25acd..6eee16e7d3 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java @@ -26,6 +26,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -78,8 +79,7 @@ public class TbDeleteKeysNodeTest { @Test void givenMsgFromMetadata_whenOnMsg_thenVerifyOutput() throws Exception { - String data = "{}"; - node.onMsg(ctx, getTbMsg(deviceId, data)); + node.onMsg(ctx, getTbMsg(deviceId, TbMsg.EMPTY_JSON_OBJECT)); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx, times(1)).tellSuccess(newMsgCaptor.capture()); @@ -140,7 +140,7 @@ public class TbDeleteKeysNodeTest { "voltageDataValue", "220", "city", "NY" ); - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(mdMap), data, callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, new TbMsgMetaData(mdMap), data, callback); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbJsonPathNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbJsonPathNodeTest.java index 0ca486a3e0..a86d902007 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbJsonPathNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbJsonPathNodeTest.java @@ -27,6 +27,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -170,6 +171,6 @@ public class TbJsonPathNodeTest { Map mdMap = Map.of("country", "US", "city", "NY" ); - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(mdMap), data, callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, new TbMsgMetaData(mdMap), data, callback); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java index 7bfa5c8bfc..be6aea6fa5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java @@ -30,7 +30,6 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.deduplication.DeduplicationStrategy; import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNode; import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNodeConfiguration; @@ -39,9 +38,10 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import java.util.ArrayList; import java.util.List; @@ -70,8 +70,6 @@ import static org.mockito.Mockito.when; @Slf4j public class TbMsgDeduplicationNodeTest { - private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg"; - private TbContext ctx; private final ThingsBoardThreadFactory factory = ThingsBoardThreadFactory.forName("de-duplication-node-test"); @@ -97,12 +95,12 @@ public class TbMsgDeduplicationNodeTest { when(ctx.getTenantId()).thenReturn(tenantId); doAnswer((Answer) invocationOnMock -> { - String type = (String) (invocationOnMock.getArguments())[1]; + TbMsgType type = (TbMsgType) (invocationOnMock.getArguments())[1]; EntityId originator = (EntityId) (invocationOnMock.getArguments())[2]; TbMsgMetaData metaData = (TbMsgMetaData) (invocationOnMock.getArguments())[3]; String data = (String) (invocationOnMock.getArguments())[4]; return TbMsg.newMsg(type, originator, metaData.copy(), data); - }).when(ctx).newMsg(isNull(), eq(TB_MSG_DEDUPLICATION_TIMEOUT_MSG), nullable(EntityId.class), any(TbMsgMetaData.class), any(String.class)); + }).when(ctx).newMsg(isNull(), eq(TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG), nullable(EntityId.class), any(TbMsgMetaData.class), any(String.class)); node = spy(new TbMsgDeduplicationNode()); config = new TbMsgDeduplicationNodeConfiguration().defaultConfiguration(); } @@ -173,7 +171,7 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(msgCount)).ack(any()); verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); - verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS), successCaptor.capture(), failureCaptor.capture()); TbMsg firstMsg = inputMsgs.get(0); TbMsg actualMsg = newMsgCaptor.getValue(); @@ -221,7 +219,7 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(msgCount)).ack(any()); verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); - verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS), successCaptor.capture(), failureCaptor.capture()); TbMsg actualMsg = newMsgCaptor.getValue(); // msg ids should be different because we create new msg before enqueueForTellNext @@ -242,7 +240,7 @@ public class TbMsgDeduplicationNodeTest { config.setInterval(deduplicationInterval); config.setStrategy(DeduplicationStrategy.ALL); - config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); + config.setOutMsgType(TbMsgType.POST_ATTRIBUTES_REQUEST.name()); config.setQueueName(DataConstants.HP_QUEUE_NAME); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctx, nodeConfiguration); @@ -263,7 +261,7 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(msgCount)).ack(any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); - verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS), successCaptor.capture(), failureCaptor.capture()); Assertions.assertEquals(1, newMsgCaptor.getAllValues().size()); TbMsg outMessage = newMsgCaptor.getAllValues().get(0); @@ -282,7 +280,7 @@ public class TbMsgDeduplicationNodeTest { config.setInterval(deduplicationInterval); config.setStrategy(DeduplicationStrategy.ALL); - config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); + config.setOutMsgType(TbMsgType.POST_ATTRIBUTES_REQUEST.name()); config.setQueueName(DataConstants.HP_QUEUE_NAME); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctx, nodeConfiguration); @@ -309,7 +307,7 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(msgCount)).ack(any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); - verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS), successCaptor.capture(), failureCaptor.capture()); List resultMsgs = newMsgCaptor.getAllValues(); Assertions.assertEquals(2, resultMsgs.size()); @@ -363,7 +361,7 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(msgCount)).ack(any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); - verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS), successCaptor.capture(), failureCaptor.capture()); List resultMsgs = newMsgCaptor.getAllValues(); Assertions.assertEquals(2, resultMsgs.size()); @@ -414,7 +412,7 @@ public class TbMsgDeduplicationNodeTest { metaData.putValue("ts", String.valueOf(ts)); return TbMsg.newMsg( DataConstants.MAIN_QUEUE_NAME, - SessionMsgType.POST_TELEMETRY_REQUEST.name(), + TbMsgType.POST_TELEMETRY_REQUEST, deviceId, metaData, JacksonUtil.toString(dataNode)); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java index 9eade69e4b..c602aab5df 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java @@ -26,6 +26,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -134,8 +135,7 @@ public class TbRenameKeysNodeTest { @Test void givenMsgDataNotJSONObject_whenOnMsg_thenVerifyOutput() throws Exception { - String data = "[]"; - TbMsg msg = getTbMsg(deviceId, data); + TbMsg msg = getTbMsg(deviceId, TbMsg.EMPTY_JSON_ARRAY); node.onMsg(ctx, msg); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); @@ -154,6 +154,6 @@ public class TbRenameKeysNodeTest { "country", "US", "city", "NY" ); - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(mdMap), data, callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, new TbMsgMetaData(mdMap), data, callback); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNodeTest.java index 67ebf6f6fc..e7e2ae16b2 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNodeTest.java @@ -27,6 +27,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -82,8 +83,7 @@ public class TbSplitArrayMsgNodeTest { @Test void givenZeroMsg_whenOnMsg_thenVerifyOutput() throws Exception { - String data = "[]"; - VerifyOutputMsg(data); + VerifyOutputMsg(TbMsg.EMPTY_JSON_ARRAY); } @Test @@ -132,6 +132,6 @@ public class TbSplitArrayMsgNodeTest { "country", "US", "city", "NY" ); - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(mdMap), data, callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, new TbMsgMetaData(mdMap), data, callback); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java index 71348a1a23..56c94fd4ff 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java @@ -29,6 +29,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -60,8 +61,8 @@ public class TbTransformMsgNodeTest { RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); - TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON,rawJson, ruleChainId, ruleNodeId); - TbMsg transformedMsg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, "{new}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, metaData, TbMsgDataType.JSON,rawJson, ruleChainId, ruleNodeId); + TbMsg transformedMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, metaData, TbMsgDataType.JSON, "{new}", ruleChainId, ruleNodeId); when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFuture(Collections.singletonList(transformedMsg))); node.onMsg(ctx, msg); @@ -80,7 +81,7 @@ public class TbTransformMsgNodeTest { RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); - TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFailedFuture(new IllegalStateException("error"))); node.onMsg(ctx, msg); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java index 957f98d721..c675b97d08 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java @@ -16,13 +16,12 @@ package org.thingsboard.rule.engine.util; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.Customer; @@ -41,7 +40,6 @@ import org.thingsboard.server.dao.user.UserService; import java.util.EnumSet; import java.util.UUID; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -60,21 +58,7 @@ public class EntitiesCustomerIdAsyncLoaderTest { EntityType.ASSET, EntityType.DEVICE ); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java index e835fa6e78..d60f2256e1 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java @@ -16,8 +16,6 @@ package org.thingsboard.rule.engine.util; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -25,6 +23,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; @@ -61,7 +60,6 @@ import org.thingsboard.server.dao.user.UserService; import java.util.EnumSet; import java.util.NoSuchElementException; import java.util.UUID; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import static org.assertj.core.api.Assertions.assertThat; @@ -75,21 +73,7 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class EntitiesFieldsAsyncLoaderTest { - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); private static EnumSet SUPPORTED_ENTITY_TYPES; private static UUID RANDOM_UUID; private static TenantId TENANT_ID; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java index 546a726f07..c54072fb53 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java @@ -16,13 +16,12 @@ package org.thingsboard.rule.engine.util; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.data.DeviceRelationsQuery; import org.thingsboard.server.common.data.Device; @@ -37,7 +36,6 @@ import org.thingsboard.server.dao.device.DeviceService; import java.util.List; import java.util.UUID; -import java.util.concurrent.Callable; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -51,21 +49,7 @@ public class EntitiesRelatedDeviceIdAsyncLoaderTest { private static final EntityId DUMMY_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoaderTest.java index b29e451eaf..e51a3704d1 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoaderTest.java @@ -17,11 +17,11 @@ package org.thingsboard.rule.engine.util; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.data.RelationsQuery; import org.thingsboard.server.common.data.Device; @@ -41,7 +41,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; -import java.util.concurrent.Callable; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -57,21 +56,7 @@ public class EntitiesRelatedEntityIdAsyncLoaderTest { private static final EntityId ASSET_ORIGINATOR_ID = new AssetId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); private TbContext ctxMock; private RelationService relationServiceMock; diff --git a/tools/pom.xml b/tools/pom.xml index 1cdd789ffb..c0b74bb0a9 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index d7572a1523..3b1e0a9e62 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index b9db930657..082b6c0536 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -41,6 +41,7 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" cache: type: "${CACHE_TYPE:redis}" @@ -183,6 +184,7 @@ queue: fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + auto_offset_reset: "${TB_QUEUE_KAFKA_AUTO_OFFSET_RESET:earliest}" # earliest, latest or none use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 3a991911ce..3d4b806d03 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index bff7adb561..6c5ddb9c52 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -68,6 +68,7 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" cache: type: "${CACHE_TYPE:redis}" @@ -168,6 +169,7 @@ queue: fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + auto_offset_reset: "${TB_QUEUE_KAFKA_AUTO_OFFSET_RESET:earliest}" # earliest, latest or none use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index e8f639ba85..efe76111f3 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index ae8f0138a7..4c0a33929a 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -41,6 +41,7 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" cache: type: "${CACHE_TYPE:redis}" @@ -249,6 +250,7 @@ queue: fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + auto_offset_reset: "${TB_QUEUE_KAFKA_AUTO_OFFSET_RESET:earliest}" # earliest, latest or none use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 85ea0c86f1..75daa483a1 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 076dde0234..67986e4bfc 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -41,6 +41,7 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" cache: type: "${CACHE_TYPE:redis}" @@ -198,6 +199,7 @@ queue: fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + auto_offset_reset: "${TB_QUEUE_KAFKA_AUTO_OFFSET_RESET:earliest}" # earliest, latest or none use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" diff --git a/transport/pom.xml b/transport/pom.xml index ad614a8d2c..203ea6cbbb 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index e5ae7ce756..c04e7d1f1f 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT transport diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index c68c9c56a8..84fdf5ddbb 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -41,6 +41,7 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" cache: type: "${CACHE_TYPE:redis}" @@ -144,6 +145,7 @@ queue: fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + auto_offset_reset: "${TB_QUEUE_KAFKA_AUTO_OFFSET_RESET:earliest}" # earliest, latest or none use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 4112e24fce..80ccca6050 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "3.5.2", + "version": "3.6.0", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index 5b5949a005..d6dc28490f 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.5.2-SNAPSHOT + 3.6.0-SNAPSHOT thingsboard org.thingsboard diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index 8f612da5a1..67e2fd7b42 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/ui-ngx/src/app/app.component.ts @@ -30,6 +30,7 @@ import { combineLatest } from 'rxjs'; import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors'; import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators'; import { AuthService } from '@core/auth/auth.service'; +import { svgIcons, svgIconsUrl } from '@shared/models/icon.models'; @Component({ selector: 'tb-root', @@ -55,44 +56,18 @@ export class AppComponent implements OnInit { } }); - this.matIconRegistry.addSvgIconLiteral( - 'google-logo', - this.domSanitizer.bypassSecurityTrustHtml( - '' - ) - ); - - this.matIconRegistry.addSvgIconLiteral( - 'github-logo', - this.domSanitizer.bypassSecurityTrustHtml( - '' - ) - ); - - this.matIconRegistry.addSvgIconLiteral( - 'facebook-logo', - this.domSanitizer.bypassSecurityTrustHtml( - '' - ) - ); - - this.matIconRegistry.addSvgIconLiteral( - 'apple-logo', - this.domSanitizer.bypassSecurityTrustHtml( - '' - ) - ); - - this.matIconRegistry.addSvgIconLiteral( - 'queues-list', - this.domSanitizer.bypassSecurityTrustHtml( - '' + - '' + - '' + - '' + - '' - ) - ); + for (const svgIcon of Object.keys(svgIcons)) { + this.matIconRegistry.addSvgIconLiteral( + svgIcon, + this.domSanitizer.bypassSecurityTrustHtml( + svgIcons[svgIcon] + ) + ); + } + + for (const svgIcon of Object.keys(svgIconsUrl)) { + this.matIconRegistry.addSvgIcon(svgIcon, this.domSanitizer.bypassSecurityTrustResourceUrl(svgIconsUrl[svgIcon])); + } this.storageService.testLocalStorage(); diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index 25cb47ca0a..1e0f31c4d0 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -42,6 +42,7 @@ import { EntityCountCmd, EntityDataCmd, IndexedSubscriptionData, + NOT_SUPPORTED, SubscriptionData, TelemetrySubscriber } from '@shared/models/telemetry/telemetry.models'; @@ -786,7 +787,7 @@ export class EntityDataSubscription { private reportNotSupported(keys: AggKey[], isUpdate: boolean) { const indexedData: IndexedSubscriptionData = []; for (const key of keys) { - indexedData[key.id] = [[0, 'Not supported!']]; + indexedData[key.id] = [[0, NOT_SUPPORTED]]; } for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { this.onIndexedData(indexedData, dataIndex, true, 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 115bf6e222..d0de3481ed 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -284,6 +284,8 @@ export interface IWidgetSubscription { legendData: LegendData; + readonly firstDatasource?: Datasource; + datasourcePages?: PageData[]; dataPages?: PageData>[]; datasources?: Array; diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 440e61148e..374775bdd0 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -81,6 +81,7 @@ import { distinct, filter, map, switchMap, takeUntil } from 'rxjs/operators'; import { AlarmDataListener } from '@core/api/alarm-data.service'; import { RpcStatus } from '@shared/models/rpc.models'; import { EventEmitter } from '@angular/core'; +import { NOT_SUPPORTED } from '@shared/models/telemetry/telemetry.models'; const moment = moment_; @@ -160,6 +161,16 @@ export class WidgetSubscription implements IWidgetSubscription { warnOnPageDataOverflow: boolean; ignoreDataUpdateOnIntervalTick: boolean; + get firstDatasource(): Datasource { + if (this.type === widgetType.alarm) { + return this.alarmSource; + } else if (this.datasources?.length) { + return this.datasources[0]; + } else { + return null; + } + } + datasourcePages: PageData[]; dataPages: PageData>[]; entityDataListeners: Array; @@ -1531,7 +1542,7 @@ export class WidgetSubscription implements IWidgetSubscription { } else if (prevData && prevData[0] && prevData[0].length > 1 && data.data.length > 0) { const prevTs = prevData[0][0]; const prevValue = prevData[0][1]; - if (prevTs === data.data[0][0] && prevValue === data.data[0][1]) { + if (prevTs === data.data[0][0] && prevValue === data.data[0][1] && data.data[0][1] !== NOT_SUPPORTED) { update = false; } } diff --git a/ui-ngx/src/app/core/auth/auth.actions.ts b/ui-ngx/src/app/core/auth/auth.actions.ts index 67b122d67f..a6bf97c527 100644 --- a/ui-ngx/src/app/core/auth/auth.actions.ts +++ b/ui-ngx/src/app/core/auth/auth.actions.ts @@ -15,17 +15,21 @@ /// import { Action } from '@ngrx/store'; -import { User } from '@shared/models/user.model'; +import { AuthUser, User } from '@shared/models/user.model'; import { AuthPayload } from '@core/auth/auth.models'; +import { UserSettings } from '@shared/models/user-settings.models'; export enum AuthActionTypes { AUTHENTICATED = '[Auth] Authenticated', UNAUTHENTICATED = '[Auth] Unauthenticated', LOAD_USER = '[Auth] Load User', UPDATE_USER_DETAILS = '[Auth] Update User Details', + UPDATE_AUTH_USER = '[Auth] Update Auth User', UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id', UPDATE_HAS_REPOSITORY = '[Auth] Change Has Repository', - UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section' + UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section', + PUT_USER_SETTINGS = '[Preferences] Put user settings', + DELETE_USER_SETTINGS = '[Preferences] Delete user settings', } export class ActionAuthAuthenticated implements Action { @@ -50,6 +54,12 @@ export class ActionAuthUpdateUserDetails implements Action { constructor(readonly payload: { userDetails: User }) {} } +export class ActionAuthUpdateAuthUser implements Action { + readonly type = AuthActionTypes.UPDATE_AUTH_USER; + + constructor(readonly payload: Partial) {} +} + export class ActionAuthUpdateLastPublicDashboardId implements Action { readonly type = AuthActionTypes.UPDATE_LAST_PUBLIC_DASHBOARD_ID; @@ -68,6 +78,19 @@ export class ActionPreferencesUpdateOpenedMenuSection implements Action { constructor(readonly payload: { path: string; opened: boolean }) {} } +export class ActionPreferencesPutUserSettings implements Action { + readonly type = AuthActionTypes.PUT_USER_SETTINGS; + + constructor(readonly payload: Partial) {} +} + +export class ActionPreferencesDeleteUserSettings implements Action { + readonly type = AuthActionTypes.DELETE_USER_SETTINGS; + + constructor(readonly payload: Array>) {} +} + export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated | ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository | - ActionPreferencesUpdateOpenedMenuSection; + ActionPreferencesUpdateOpenedMenuSection | ActionPreferencesPutUserSettings | ActionPreferencesDeleteUserSettings | + ActionAuthUpdateAuthUser; diff --git a/ui-ngx/src/app/core/auth/auth.effects.ts b/ui-ngx/src/app/core/auth/auth.effects.ts index d2ebe2b4c5..3e5eb28d72 100644 --- a/ui-ngx/src/app/core/auth/auth.effects.ts +++ b/ui-ngx/src/app/core/auth/auth.effects.ts @@ -39,4 +39,18 @@ export class AuthEffects { withLatestFrom(this.store.pipe(select(selectAuthState))), mergeMap(([action, state]) => this.userSettingsService.putUserSettings({ openedMenuSections: state.userSettings.openedMenuSections })) ), {dispatch: false}); + + putUserSettings = createEffect(() => this.actions$.pipe( + ofType( + AuthActionTypes.PUT_USER_SETTINGS, + ), + mergeMap((state) => this.userSettingsService.putUserSettings(state.payload)) + ), {dispatch: false}); + + deleteUserSettings = createEffect(() => this.actions$.pipe( + ofType( + AuthActionTypes.DELETE_USER_SETTINGS, + ), + mergeMap((state) => this.userSettingsService.deleteUserSettings(state.payload)) + ), {dispatch: false}); } diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index 0847654399..c0ce66e990 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -16,7 +16,8 @@ import { AuthPayload, AuthState } from './auth.models'; import { AuthActions, AuthActionTypes } from './auth.actions'; -import { initialUserSettings } from '@shared/models/user-settings.models'; +import { initialUserSettings, UserSettings } from '@shared/models/user-settings.models'; +import { unset } from '@core/utils'; const emptyUserAuthState: AuthPayload = { authUser: null, @@ -42,6 +43,7 @@ export const authReducer = ( state: AuthState = initialState, action: AuthActions ): AuthState => { + let userSettings: UserSettings; switch (action.type) { case AuthActionTypes.AUTHENTICATED: return { ...state, isAuthenticated: true, ...action.payload }; @@ -56,6 +58,10 @@ export const authReducer = ( case AuthActionTypes.UPDATE_USER_DETAILS: return { ...state, ...action.payload}; + case AuthActionTypes.UPDATE_AUTH_USER: + const authUser = {...state.authUser, ...action.payload}; + return { ...state, ...{ authUser }}; + case AuthActionTypes.UPDATE_LAST_PUBLIC_DASHBOARD_ID: return { ...state, ...action.payload}; @@ -71,7 +77,16 @@ export const authReducer = ( } else { openedMenuSections.delete(action.payload.path); } - const userSettings = {...state.userSettings, ...{ openedMenuSections: Array.from(openedMenuSections)}}; + userSettings = {...state.userSettings, ...{ openedMenuSections: Array.from(openedMenuSections)}}; + return { ...state, ...{ userSettings }}; + + case AuthActionTypes.PUT_USER_SETTINGS: + userSettings = {...state.userSettings, ...action.payload}; + return { ...state, ...{ userSettings }}; + + case AuthActionTypes.DELETE_USER_SETTINGS: + userSettings = {...state.userSettings}; + action.payload.forEach(path => unset(userSettings, path)); return { ...state, ...{ userSettings }}; default: diff --git a/ui-ngx/src/app/core/auth/auth.selectors.ts b/ui-ngx/src/app/core/auth/auth.selectors.ts index 2e8406293e..4bf2f2276d 100644 --- a/ui-ngx/src/app/core/auth/auth.selectors.ts +++ b/ui-ngx/src/app/core/auth/auth.selectors.ts @@ -21,6 +21,7 @@ import { AuthState } from './auth.models'; import { take } from 'rxjs/operators'; import { AuthUser } from '@shared/models/user.model'; import { UserSettings } from '@shared/models/user-settings.models'; +import { getDescendantProp } from '@core/utils'; export const selectAuthState = createFeatureSelector< AuthState>( 'auth' @@ -76,6 +77,11 @@ export const selectUserSettings = createSelector( (state: AuthState) => state.userSettings ); +export const selectUserSettingsProperty = (path: NestedKeyOf) => createSelector( + selectAuthState, + (state: AuthState) => getDescendantProp(state.userSettings, path) +); + export const selectOpenedMenuSections = createSelector( selectAuthState, (state: AuthState) => state.userSettings.openedMenuSections diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 87c6561315..e969c8129a 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -27,7 +27,12 @@ import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from import { UserService } from '../http/user.service'; import { Store } from '@ngrx/store'; import { AppState } from '../core.state'; -import { ActionAuthAuthenticated, ActionAuthLoadUser, ActionAuthUnauthenticated } from './auth.actions'; +import { + ActionAuthAuthenticated, + ActionAuthLoadUser, + ActionAuthUnauthenticated, + ActionAuthUpdateAuthUser +} from './auth.actions'; import { getCurrentAuthState, getCurrentAuthUser } from './auth.selectors'; import { Authority } from '@shared/models/authority.enum'; import { ActionSettingsChangeLanguage } from '@app/core/settings/settings.actions'; @@ -244,7 +249,7 @@ export class AuthService { if (authState && authState.authUser) { if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) { if ((this.userHasDefaultDashboard(authState) && authState.forceFullscreen) || authState.authUser.isPublic) { - if (path === 'profile' || path === 'security') { + if (path.startsWith('account')) { if (this.userHasProfile(authState.authUser)) { return false; } else { @@ -385,10 +390,10 @@ export class AuthService { } else if (authPayload.authUser) { authPayload.authUser.authority = Authority.ANONYMOUS; } - if (authPayload.authUser.isPublic) { + if (authPayload.authUser?.isPublic) { authPayload.forceFullscreen = true; } - if (authPayload.authUser.isPublic) { + if (authPayload.authUser?.isPublic) { this.loadSystemParams().subscribe( (sysParams) => { authPayload = {...authPayload, ...sysParams}; @@ -399,10 +404,10 @@ export class AuthService { loadUserSubject.error(err); } ); - } else if (authPayload.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) { + } else if (authPayload.authUser?.authority === Authority.PRE_VERIFICATION_TOKEN) { loadUserSubject.next(authPayload); loadUserSubject.complete(); - } else if (authPayload.authUser.userId) { + } else if (authPayload.authUser?.userId) { this.userService.getUser(authPayload.authUser.userId).subscribe( (user) => { authPayload.userDetails = user; @@ -480,6 +485,7 @@ export class AuthService { } else { this.updateAndValidateTokens(loginResponse.token, loginResponse.refreshToken, true); } + this.updatedAuthUserFromToken(loginResponse.token); this.refreshTokenSubject.next(loginResponse); this.refreshTokenSubject.complete(); this.refreshTokenSubject = null; @@ -493,6 +499,18 @@ export class AuthService { return response; } + private updatedAuthUserFromToken(token: string) { + const authUser = getCurrentAuthUser(this.store); + const tokenData = this.jwtHelper.decodeToken(token); + if (['sub', 'firstName', 'lastName'].some(value => authUser[value] !== tokenData[value])) { + this.store.dispatch(new ActionAuthUpdateAuthUser({ + sub: tokenData.sub, + firstName: tokenData.firstName, + lastName: tokenData.lastName, + })); + } + } + private validateJwtToken(doRefresh): Observable { const subject = new ReplaySubject(); if (!AuthService.isTokenValid('jwt_token')) { diff --git a/ui-ngx/src/app/core/http/alarm.service.ts b/ui-ngx/src/app/core/http/alarm.service.ts index e03e9ab783..dc68a092b3 100644 --- a/ui-ngx/src/app/core/http/alarm.service.ts +++ b/ui-ngx/src/app/core/http/alarm.service.ts @@ -28,7 +28,8 @@ import { AlarmSeverity, AlarmStatus } from '@shared/models/alarm.models'; -import { UtilsService } from '@core/services/utils.service'; +import { EntitySubtype } from '@shared/models/entity-type.models'; +import { PageLink } from '@shared/models/page/page-link'; @Injectable({ providedIn: 'root' @@ -36,8 +37,7 @@ import { UtilsService } from '@core/services/utils.service'; export class AlarmService { constructor( - private http: HttpClient, - private utils: UtilsService + private http: HttpClient ) { } public getAlarm(alarmId: string, config?: RequestConfig): Observable { @@ -52,12 +52,12 @@ export class AlarmService { return this.http.post('/api/alarm', alarm, defaultHttpOptionsFromConfig(config)); } - public ackAlarm(alarmId: string, config?: RequestConfig): Observable { - return this.http.post(`/api/alarm/${alarmId}/ack`, null, defaultHttpOptionsFromConfig(config)); + public ackAlarm(alarmId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/alarm/${alarmId}/ack`, null, defaultHttpOptionsFromConfig(config)); } - public clearAlarm(alarmId: string, config?: RequestConfig): Observable { - return this.http.post(`/api/alarm/${alarmId}/clear`, null, defaultHttpOptionsFromConfig(config)); + public clearAlarm(alarmId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/alarm/${alarmId}/clear`, null, defaultHttpOptionsFromConfig(config)); } public assignAlarm(alarmId: string, assigneeId: string, config?: RequestConfig): Observable { @@ -68,8 +68,8 @@ export class AlarmService { return this.http.delete(`/api/alarm/${alarmId}/assign`, defaultHttpOptionsFromConfig(config)); } - public deleteAlarm(alarmId: string, config?: RequestConfig): Observable { - return this.http.delete(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config)); + public deleteAlarm(alarmId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config)); } public getAlarms(query: AlarmQuery, @@ -108,4 +108,8 @@ export class AlarmService { defaultHttpOptionsFromConfig(config)); } + public getAlarmTypes(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/types${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/http/attribute.service.ts b/ui-ngx/src/app/core/http/attribute.service.ts index 67132d8983..9022a47eef 100644 --- a/ui-ngx/src/app/core/http/attribute.service.ts +++ b/ui-ngx/src/app/core/http/attribute.service.ts @@ -50,10 +50,19 @@ export class AttributeService { } public deleteEntityTimeseries(entityId: EntityId, timeseries: Array, deleteAllDataForKeys = false, - startTs?: number, endTs?: number, config?: RequestConfig): Observable { + startTs?: number, endTs?: number, rewriteLatestIfDeleted = false, deleteLatest = true, + config?: RequestConfig): Observable { const keys = timeseries.map(attribute => encodeURIComponent(attribute.key)).join(','); - let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` + - `?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`; + let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete?keys=${keys}`; + if (isDefinedAndNotNull(deleteAllDataForKeys)) { + url += `&deleteAllDataForKeys=${deleteAllDataForKeys}`; + } + if (isDefinedAndNotNull(rewriteLatestIfDeleted)) { + url += `&rewriteLatestIfDeleted=${rewriteLatestIfDeleted}`; + } + if (isDefinedAndNotNull(deleteLatest)) { + url += `&deleteLatest=${deleteLatest}`; + } if (isDefinedAndNotNull(startTs)) { url += `&startTs=${startTs}`; } @@ -103,7 +112,8 @@ export class AttributeService { }); let deleteEntityTimeseriesObservable: Observable; if (deleteTimeseries.length) { - deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, null, null, config); + deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, + null, null, false, true, config); } else { deleteEntityTimeseriesObservable = of(null); } diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index dfc2d674a2..8dff1e7ebc 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -25,8 +25,10 @@ import { ClaimResult, Device, DeviceCredentials, - DeviceInfo, DeviceInfoQuery, - DeviceSearchQuery + DeviceInfo, + DeviceInfoQuery, + DeviceSearchQuery, + PublishTelemetryCommand } from '@app/shared/models/device.models'; import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { AuthService } from '@core/auth/auth.service'; @@ -208,4 +210,8 @@ export class DeviceService { return this.http.post('/api/device/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config)); } + public getDevicePublishTelemetryCommands(deviceId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/device-connectivity/${deviceId}`, defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/http/notification.service.ts b/ui-ngx/src/app/core/http/notification.service.ts index ec64621aeb..20e10c91e2 100644 --- a/ui-ngx/src/app/core/http/notification.service.ts +++ b/ui-ngx/src/app/core/http/notification.service.ts @@ -31,6 +31,7 @@ import { NotificationTarget, NotificationTemplate, NotificationType, + NotificationUserSettings, SlackChanelType, SlackConversation } from '@shared/models/notification.models'; @@ -174,4 +175,12 @@ export class NotificationService { } return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } + + public getNotificationUserSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/notification/settings/user`, defaultHttpOptionsFromConfig(config)); + } + + public saveNotificationUserSettings(settings: NotificationUserSettings, config?: RequestConfig): Observable { + return this.http.post('/api/notification/settings/user', settings, defaultHttpOptionsFromConfig(config)); + } } diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 5f5f0f42df..6affb01efe 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -22,16 +22,15 @@ import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { - Widget, + BaseWidgetType, + fullWidgetTypeFqn, WidgetType, widgetType, WidgetTypeDetails, WidgetTypeInfo, widgetTypesData } from '@shared/models/widget.models'; -import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; -import { ResourcesService } from '../services/resources.service'; import { toWidgetInfo, toWidgetTypeDetails, WidgetInfo } from '@app/modules/home/models/widget-component.models'; import { filter, map, mergeMap, tap } from 'rxjs/operators'; import { WidgetTypeId } from '@shared/models/id/widget-type-id'; @@ -55,8 +54,6 @@ export class WidgetService { constructor( private http: HttpClient, - private utils: UtilsService, - private resources: ResourcesService, private translate: TranslateService, private router: Router ) { @@ -109,108 +106,77 @@ export class WidgetService { ); } + public updateWidgetsBundleWidgetTypes(widgetsBundleId: string, widgetTypeIds: Array, + config?: RequestConfig): Observable { + return this.http.post(`/api/widgetsBundle/${widgetsBundleId}/widgetTypes`, widgetTypeIds, + defaultHttpOptionsFromConfig(config)).pipe( + tap(() => { + this.widgetTypeInfosCache.delete(widgetsBundleId); + }) + ); + } + + public updateWidgetsBundleWidgetFqns(widgetsBundleId: string, widgetTypeFqns: Array, + config?: RequestConfig): Observable { + return this.http.post(`/api/widgetsBundle/${widgetsBundleId}/widgetTypeFqns`, widgetTypeFqns, + defaultHttpOptionsFromConfig(config)).pipe( + tap(() => { + this.widgetTypeInfosCache.delete(widgetsBundleId); + }) + ); + } + public deleteWidgetsBundle(widgetsBundleId: string, config?: RequestConfig) { return this.getWidgetsBundle(widgetsBundleId, config).pipe( mergeMap((widgetsBundle) => this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, defaultHttpOptionsFromConfig(config)).pipe( tap(() => { this.invalidateWidgetsBundleCache(); - this.widgetsBundleDeleted(widgetsBundle); }) ) )); } - public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean, + public getBundleWidgetTypes(widgetsBundleId: string, config?: RequestConfig): Observable> { - return this.http.get>(`/api/widgetTypes?isSystem=${isSystem}&bundleAlias=${bundleAlias}`, + return this.http.get>(`/api/widgetTypes?widgetsBundleId=${widgetsBundleId}`, defaultHttpOptionsFromConfig(config)); } - public getBundleWidgetTypesDetails(bundleAlias: string, isSystem: boolean, + public getBundleWidgetTypesDetails(widgetsBundleId: string, config?: RequestConfig): Observable> { - return this.http.get>(`/api/widgetTypesDetails?isSystem=${isSystem}&bundleAlias=${bundleAlias}`, + return this.http.get>(`/api/widgetTypesDetails?widgetsBundleId=${widgetsBundleId}`, defaultHttpOptionsFromConfig(config)); } - public getBundleWidgetTypeInfos(bundleAlias: string, isSystem: boolean, + public getBundleWidgetTypeFqns(widgetsBundleId: string, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/widgetTypeFqns?widgetsBundleId=${widgetsBundleId}`, + defaultHttpOptionsFromConfig(config)); + } + + public getBundleWidgetTypeInfos(widgetsBundleId: string, config?: RequestConfig): Observable> { - const key = bundleAlias + (isSystem ? '_sys' : ''); - if (this.widgetTypeInfosCache.has(key)) { - return of(this.widgetTypeInfosCache.get(key)); + if (this.widgetTypeInfosCache.has(widgetsBundleId)) { + return of(this.widgetTypeInfosCache.get(widgetsBundleId)); } else { - return this.http.get>(`/api/widgetTypesInfos?isSystem=${isSystem}&bundleAlias=${bundleAlias}`, + return this.http.get>(`/api/widgetTypesInfos?widgetsBundleId=${widgetsBundleId}`, defaultHttpOptionsFromConfig(config)).pipe( - tap((res) => this.widgetTypeInfosCache.set(key, res) ) + tap((res) => this.widgetTypeInfosCache.set(widgetsBundleId, res) ) ); } } - public loadBundleLibraryWidgets(bundleAlias: string, isSystem: boolean, - config?: RequestConfig): Observable> { - return this.getBundleWidgetTypes(bundleAlias, isSystem, config).pipe( - map((types) => { - types = types.sort((a, b) => { - let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]); - if (result === 0) { - result = b.createdTime - a.createdTime; - } - return result; - }); - const widgetTypes = new Array(); - let top = 0; - const lastTop = [0, 0, 0]; - let col = 0; - let column = 0; - types.forEach((type) => { - const widgetTypeInfo = toWidgetInfo(type); - const sizeX = 8; - const sizeY = Math.floor(widgetTypeInfo.sizeY); - const widget: Widget = { - typeId: type.id, - isSystemType: isSystem, - bundleAlias, - typeAlias: widgetTypeInfo.alias, - type: widgetTypeInfo.type, - title: widgetTypeInfo.widgetName, - sizeX, - sizeY, - row: top, - col, - config: JSON.parse(widgetTypeInfo.defaultConfig) - }; - - widget.config.title = widgetTypeInfo.widgetName; - - widgetTypes.push(widget); - top += sizeY; - if (top > lastTop[column] + 10) { - lastTop[column] = top; - column++; - if (column > 2) { - column = 0; - } - top = lastTop[column]; - col = column * 8; - } - }); - return widgetTypes; - }) - ); - } - - public getWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean, - config?: RequestConfig): Observable { - return this.http.get(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`, + public getWidgetType(fullFqn: string, config?: RequestConfig): Observable { + return this.http.get(`/api/widgetType?fqn=${fullFqn}`, defaultHttpOptionsFromConfig(config)); } public saveWidgetTypeDetails(widgetInfo: WidgetInfo, id: WidgetTypeId, - bundleAlias: string, createdTime: number, config?: RequestConfig): Observable { - const widgetTypeDetails = toWidgetTypeDetails(widgetInfo, id, undefined, bundleAlias, createdTime); + const widgetTypeDetails = toWidgetTypeDetails(widgetInfo, id, undefined, createdTime); return this.http.post('/api/widgetType', widgetTypeDetails, defaultHttpOptionsFromConfig(config)).pipe( tap((savedWidgetType) => { @@ -220,78 +186,77 @@ export class WidgetService { public saveImportedWidgetTypeDetails(widgetTypeDetails: WidgetTypeDetails, config?: RequestConfig): Observable { - return this.http.post('/api/widgetType', widgetTypeDetails, + return this.http.post('/api/widgetType?updateExistingByFqn=true', widgetTypeDetails, defaultHttpOptionsFromConfig(config)).pipe( tap((savedWidgetType) => { this.widgetTypeUpdated(savedWidgetType); })); } - public deleteWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean, - config?: RequestConfig) { - return this.getWidgetType(bundleAlias, widgetTypeAlias, isSystem, config).pipe( - mergeMap((widgetTypeInstance) => this.http.delete(`/api/widgetType/${widgetTypeInstance.id.id}`, - defaultHttpOptionsFromConfig(config)).pipe( - tap(() => { - this.widgetTypeUpdated(widgetTypeInstance); - }) - ) - )); - } - public getWidgetTypeById(widgetTypeId: string, config?: RequestConfig): Observable { return this.http.get(`/api/widgetType/${widgetTypeId}`, defaultHttpOptionsFromConfig(config)); } + public getWidgetTypeInfoById(widgetTypeId: string, + config?: RequestConfig): Observable { + return this.http.get(`/api/widgetTypeInfo/${widgetTypeId}`, + defaultHttpOptionsFromConfig(config)); + } + + public saveWidgetType(widgetTypeDetails: WidgetTypeDetails, + config?: RequestConfig): Observable { + return this.http.post(`/api/widgetType`, widgetTypeDetails, + defaultHttpOptionsFromConfig(config)); + } + + public deleteWidgetType(widgetTypeId: string, + config?: RequestConfig) { + return this.getWidgetTypeById(widgetTypeId, config).pipe( + mergeMap((widgetTypeDetails) => + this.http.delete(`/api/widgetType/${widgetTypeId}`, defaultHttpOptionsFromConfig(config)).pipe( + tap(() => { + this.widgetTypeUpdated(widgetTypeDetails); + }) + ) + )); + } + + public getWidgetTypes(pageLink: PageLink, tenantOnly = false, + fullSearch = false, config?: RequestConfig): Observable> { + return this.http.get>( + `/api/widgetTypes${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}`, + defaultHttpOptionsFromConfig(config)); + } + public getWidgetTemplate(widgetTypeParam: widgetType, config?: RequestConfig): Observable { const templateWidgetType = widgetTypesData.get(widgetTypeParam); - return this.getWidgetType(templateWidgetType.template.bundleAlias, templateWidgetType.template.alias, true, + return this.getWidgetType(templateWidgetType.template.fullFqn, config).pipe( map((result) => { const widgetInfo = toWidgetInfo(result); - widgetInfo.alias = undefined; + widgetInfo.fullFqn = undefined; return widgetInfo; }) ); } - public createWidgetInfoCacheKey(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): string { - return `${isSystem ? 'sys_' : ''}${bundleAlias}_${widgetTypeAlias}`; - } - - public getWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): WidgetInfo | undefined { - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - return this.widgetsInfoInMemoryCache.get(key); + public getWidgetInfoFromCache(fullFqn: string): WidgetInfo | undefined { + return this.widgetsInfoInMemoryCache.get(fullFqn); } - public putWidgetInfoToCache(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - this.widgetsInfoInMemoryCache.set(key, widgetInfo); + public putWidgetInfoToCache(widgetInfo: WidgetInfo) { + this.widgetsInfoInMemoryCache.set(widgetInfo.fullFqn, widgetInfo); } - private widgetTypeUpdated(updatedWidgetType: WidgetType): void { - this.deleteWidgetInfoFromCache(updatedWidgetType.bundleAlias, updatedWidgetType.alias, updatedWidgetType.tenantId.id === NULL_UUID); + private widgetTypeUpdated(updatedWidgetType: BaseWidgetType): void { + this.deleteWidgetInfoFromCache(fullWidgetTypeFqn(updatedWidgetType)); } - private widgetsBundleDeleted(widgetsBundle: WidgetsBundle): void { - this.deleteWidgetsBundleFromCache(widgetsBundle.alias, widgetsBundle.tenantId.id === NULL_UUID); - } - - public deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - this.widgetsInfoInMemoryCache.delete(key); - } - - private deleteWidgetsBundleFromCache(bundleAlias: string, isSystem: boolean) { - const key = (isSystem ? 'sys_' : '') + bundleAlias; - this.widgetsInfoInMemoryCache.forEach((widgetInfo, cacheKey) => { - if (cacheKey.startsWith(key)) { - this.widgetsInfoInMemoryCache.delete(cacheKey); - } - }); + public deleteWidgetInfoFromCache(fullFqn: string) { + this.widgetsInfoInMemoryCache.delete(fullFqn); } private loadWidgetsBundleCache(config?: RequestConfig): Observable { 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 8355ce2f61..bb4c738516 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -29,13 +29,20 @@ import { GridSettings, WidgetLayout } from '@shared/models/dashboard.models'; -import { deepClone, isDefined, isDefinedAndNotNull, isString, isUndefined } from '@core/utils'; import { - DataKey, + deepClone, + isDefined, + isDefinedAndNotNull, + isEmptyStr, + isNotEmptyStr, + isString, + isUndefined +} from '@core/utils'; +import { Datasource, datasourcesHasOnlyComparisonAggregation, DatasourceType, - defaultLegendConfig, + defaultLegendConfig, isValidWidgetFullFqn, Widget, WidgetConfig, WidgetConfigMode, @@ -215,12 +222,20 @@ export class DashboardUtilsService { public validateAndUpdateWidget(widget: Widget): Widget { widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type); - // Temp workaround - if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') { - widget.typeAlias = 'basic_timeseries'; + widget = this.validateAndUpdateWidgetTypeFqn(widget); + if (isDefined((widget as any).title)) { + delete (widget as any).title; + } + if (isDefined((widget as any).image)) { + delete (widget as any).image; } - if (widget.isSystemType && widget.bundleAlias === 'charts' && - ['state_chart', 'basic_timeseries', 'timeseries_bars_flot'].includes(widget.typeAlias)) { + if (isDefined((widget as any).description)) { + delete (widget as any).description; + } + // Temp workaround + if (['system.charts.state_chart', + 'system.charts.basic_timeseries', + 'system.charts.timeseries_bars_flot'].includes(widget.typeFullFqn)) { const widgetConfig = widget.config; const widgetSettings = widget.config.settings; if (isDefinedAndNotNull(widgetConfig.showLegend)) { @@ -239,6 +254,29 @@ export class DashboardUtilsService { return widget; } + private validateAndUpdateWidgetTypeFqn(widget: Widget): Widget { + const w = widget as any; + if (!isValidWidgetFullFqn(widget.typeFullFqn)) { + if (isDefinedAndNotNull(w.isSystemType) && isNotEmptyStr(w.bundleAlias) && isNotEmptyStr(w.typeAlias)) { + widget.typeFullFqn = (w.isSystemType ? 'system' : 'tenant') + '.' + w.bundleAlias + '.' + w.typeAlias; + } + } + if (isDefined(w.isSystemType)) { + delete w.isSystemType; + } + if (isDefined(w.bundleAlias)) { + delete w.bundleAlias; + } + if (isDefined(w.typeAlias)) { + delete w.typeAlias; + } + // Temp workaround + if (widget.typeFullFqn === 'system.charts.timeseries') { + widget.typeFullFqn = 'system.charts.basic_timeseries'; + } + return widget; + } + public validateAndUpdateWidgetConfig(widgetConfig: WidgetConfig | undefined, type: widgetType): WidgetConfig { if (!widgetConfig) { widgetConfig = {}; @@ -348,10 +386,8 @@ export class DashboardUtilsService { private convertDatasourcesFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor, config: WidgetConfig, datasources?: Datasource[]): Datasource[] { const newDatasources: Datasource[] = []; - if (datasources) { - datasources.forEach(datasource => { - newDatasources.push(this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, datasource)); - }); + if (datasources?.length) { + newDatasources.push(this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, datasources[0])); } return newDatasources; } @@ -486,6 +522,14 @@ export class DashboardUtilsService { return widgetsArray; } + public isEmptyDashboard(dashboard: Dashboard): boolean { + if (dashboard?.configuration?.widgets) { + return Object.keys(dashboard?.configuration?.widgets).length === 0; + } else { + return true; + } + } + public addWidgetToLayout(dashboard: Dashboard, targetState: string, targetLayout: DashboardLayoutId, diff --git a/ui-ngx/src/app/core/services/dialog.service.ts b/ui-ngx/src/app/core/services/dialog.service.ts index 2ca2d6d87e..c5f54f9cf0 100644 --- a/ui-ngx/src/app/core/services/dialog.service.ts +++ b/ui-ngx/src/app/core/services/dialog.service.ts @@ -21,11 +21,11 @@ import { TranslateService } from '@ngx-translate/core'; import { AuthService } from '@core/auth/auth.service'; import { ColorPickerDialogComponent, - ColorPickerDialogData + ColorPickerDialogData, ColorPickerDialogResult } from '@shared/components/dialog/color-picker-dialog.component'; import { MaterialIconsDialogComponent, - MaterialIconsDialogData + MaterialIconsDialogData, MaterialIconsDialogResult } from '@shared/components/dialog/material-icons-dialog.component'; import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; @@ -96,25 +96,27 @@ export class DialogService { return dialogRef.afterClosed(); } - colorPicker(color: string): Observable { - return this.dialog.open(ColorPickerDialogComponent, + colorPicker(color: string, colorClearButton = false): Observable { + return this.dialog.open(ColorPickerDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - color + color, + colorClearButton }, autoFocus: false }).afterClosed(); } - materialIconPicker(icon: string): Observable { - return this.dialog.open(MaterialIconsDialogComponent, + materialIconPicker(icon: string, iconClearButton = false): Observable { + return this.dialog.open(MaterialIconsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - icon + icon, + iconClearButton }, autoFocus: false }).afterClosed(); diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts index 84134f04a9..b7e21f7b22 100644 --- a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts +++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts @@ -33,11 +33,17 @@ import { catchError, map, mergeMap } from 'rxjs/operators'; @NgModule() export abstract class DynamicComponentModule implements OnDestroy { + // eslint-disable-next-line @angular-eslint/contextual-lifecycle ngOnDestroy(): void { } } +interface DynamicComponentData { + componentType: Type; + componentModuleRef: NgModuleRef; +} + interface DynamicComponentModuleData { moduleRef: NgModuleRef; moduleType: Type; @@ -48,22 +54,22 @@ interface DynamicComponentModuleData { }) export class DynamicComponentFactoryService { - private dynamicComponentModulesMap = new Map, DynamicComponentModuleData>(); + private dynamicComponentModulesMap = new Map, DynamicComponentModuleData>(); constructor(private compiler: Compiler, private injector: Injector) { } - public createDynamicComponentFactory( + public createDynamicComponent( componentType: Type, template: string, modules?: Type[], preserveWhitespaces?: boolean, compileAttempt = 1, - styles?: string[]): Observable> { + styles?: string[]): Observable> { return from(import('@angular/compiler')).pipe( mergeMap(() => { - const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces, styles); + const comp = this._createDynamicComponent(componentType, template, preserveWhitespaces, styles); let moduleImports: Type[] = [CommonModule]; if (modules) { moduleImports = [...moduleImports, ...modules]; @@ -82,17 +88,19 @@ export class DynamicComponentFactoryService { this.compiler.clearCacheFor(module.moduleType); throw e; } - const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp); - this.dynamicComponentModulesMap.set(factory, { + this.dynamicComponentModulesMap.set(comp, { moduleRef, moduleType: module.moduleType }); - return factory; + return { + componentType: comp, + componentModuleRef: moduleRef + }; }), catchError((error) => { if (compileAttempt === 1) { ɵresetCompiledComponents(); - return this.createDynamicComponentFactory(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles); + return this.createDynamicComponent(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles); } else { throw error; } @@ -102,16 +110,16 @@ export class DynamicComponentFactoryService { ); } - public destroyDynamicComponentFactory(factory: ComponentFactory) { - const moduleData = this.dynamicComponentModulesMap.get(factory); + public destroyDynamicComponent(componentType: Type) { + const moduleData = this.dynamicComponentModulesMap.get(componentType); if (moduleData) { moduleData.moduleRef.destroy(); this.compiler.clearCacheFor(moduleData.moduleType); - this.dynamicComponentModulesMap.delete(factory); + this.dynamicComponentModulesMap.delete(componentType); } } - private createDynamicComponent(componentType: Type, template: string, preserveWhitespaces?: boolean, styles?: string[]): Type { + private _createDynamicComponent(componentType: Type, template: string, preserveWhitespaces?: boolean, styles?: string[]): Type { // noinspection AngularMissingOrInvalidDeclarationInModule return Component({ template, diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 2043eeadea..47072943be 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -24,7 +24,6 @@ export interface MenuSection extends HasUUID{ type: MenuSectionType; path: string; icon: string; - isMdiIcon?: boolean; pages?: Array; opened?: boolean; disabled?: boolean; @@ -39,6 +38,5 @@ export interface HomeSection { export interface HomeSectionPlace { name: string; icon: string; - isMdiIcon?: boolean; path: string; } diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index b33c552eb1..d48b0619ca 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -111,8 +111,7 @@ export class MenuService { name: 'tenant-profile.tenant-profiles', type: 'link', path: '/tenantProfiles', - icon: 'mdi:alpha-t-box', - isMdiIcon: true + icon: 'mdi:alpha-t-box' }, { id: 'resources', @@ -125,16 +124,31 @@ export class MenuService { id: 'widget_library', name: 'widget.widget-library', type: 'link', - path: '/resources/widgets-bundles', - icon: 'now_widgets' + path: '/resources/widgets-library', + icon: 'now_widgets', + pages: [ + { + id: 'widget_types', + name: 'widget.widgets', + type: 'link', + path: '/resources/widgets-library/widget-types', + icon: 'now_widgets' + }, + { + id: 'widgets_bundles', + name: 'widgets-bundle.widgets-bundles', + type: 'link', + path: '/resources/widgets-library/widgets-bundles', + icon: 'now_widgets' + } + ] }, { id: 'resources_library', name: 'resource.resources-library', type: 'link', path: '/resources/resources-library', - icon: 'mdi:rhombus-split', - isMdiIcon: true + icon: 'mdi:rhombus-split' } ] }, @@ -144,7 +158,6 @@ export class MenuService { type: 'link', path: '/notification', icon: 'mdi:message-badge', - isMdiIcon: true, pages: [ { id: 'notification_inbox', @@ -176,8 +189,7 @@ export class MenuService { fullName: 'notification.notification-templates', type: 'link', path: '/notification/templates', - icon: 'mdi:message-draw', - isMdiIcon: true + icon: 'mdi:message-draw' }, { id: 'notification_rules', @@ -185,8 +197,7 @@ export class MenuService { fullName: 'notification.notification-rules', type: 'link', path: '/notification/rules', - icon: 'mdi:message-cog', - isMdiIcon: true + icon: 'mdi:message-cog' } ] }, @@ -218,8 +229,7 @@ export class MenuService { fullName: 'admin.notifications-settings', type: 'link', path: '/settings/notifications', - icon: 'mdi:message-badge', - isMdiIcon: true + icon: 'mdi:message-badge' }, { id: 'queues', @@ -250,16 +260,14 @@ export class MenuService { name: 'admin.2fa.2fa', type: 'link', path: '/security-settings/2fa', - icon: 'mdi:two-factor-authentication', - isMdiIcon: true + icon: 'mdi:two-factor-authentication' }, { id: 'oauth2', name: 'admin.oauth2.oauth2', type: 'link', path: '/security-settings/oauth2', - icon: 'mdi:shield-account', - isMdiIcon: true + icon: 'mdi:shield-account' } ] } @@ -281,7 +289,6 @@ export class MenuService { { name: 'tenant-profile.tenant-profiles', icon: 'mdi:alpha-t-box', - isMdiIcon: true, path: '/tenantProfiles' }, ] @@ -292,7 +299,7 @@ export class MenuService { { name: 'widget.widget-library', icon: 'now_widgets', - path: '/widgets-bundles' + path: '/resources/widgets-library', } ] }, @@ -327,7 +334,6 @@ export class MenuService { { name: 'admin.2fa.2fa', icon: 'mdi:two-factor-authentication', - isMdiIcon: true, path: '/settings/2fa' }, { @@ -361,8 +367,7 @@ export class MenuService { name: 'alarm.alarms', type: 'link', path: '/alarms', - icon: 'mdi:alert-outline', - isMdiIcon: true + icon: 'mdi:alert-outline' }, { id: 'dashboards', @@ -413,16 +418,14 @@ export class MenuService { name: 'device-profile.device-profiles', type: 'link', path: '/profiles/deviceProfiles', - icon: 'mdi:alpha-d-box', - isMdiIcon: true + icon: 'mdi:alpha-d-box' }, { id: 'asset_profiles', name: 'asset-profile.asset-profiles', type: 'link', path: '/profiles/assetProfiles', - icon: 'mdi:alpha-a-box', - isMdiIcon: true + icon: 'mdi:alpha-a-box' } ] }, @@ -505,16 +508,31 @@ export class MenuService { id: 'widget_library', name: 'widget.widget-library', type: 'link', - path: '/resources/widgets-bundles', - icon: 'now_widgets' + path: '/resources/widgets-library', + icon: 'now_widgets', + pages: [ + { + id: 'widget_types', + name: 'widget.widgets', + type: 'link', + path: '/resources/widgets-library/widget-types', + icon: 'now_widgets' + }, + { + id: 'widgets_bundles', + name: 'widgets-bundle.widgets-bundles', + type: 'link', + path: '/resources/widgets-library/widgets-bundles', + icon: 'now_widgets' + } + ] }, { id: 'resources_library', name: 'resource.resources-library', type: 'link', path: '/resources/resources-library', - icon: 'mdi:rhombus-split', - isMdiIcon: true + icon: 'mdi:rhombus-split' } ] }, @@ -524,7 +542,6 @@ export class MenuService { type: 'link', path: '/notification', icon: 'mdi:message-badge', - isMdiIcon: true, pages: [ { id: 'notification_inbox', @@ -556,8 +573,7 @@ export class MenuService { fullName: 'notification.notification-templates', type: 'link', path: '/notification/templates', - icon: 'mdi:message-draw', - isMdiIcon: true + icon: 'mdi:message-draw' }, { id: 'notification_rules', @@ -565,8 +581,7 @@ export class MenuService { fullName: 'notification.notification-rules', type: 'link', path: '/notification/rules', - icon: 'mdi:message-cog', - isMdiIcon: true + icon: 'mdi:message-cog' } ] }, @@ -598,8 +613,7 @@ export class MenuService { fullName: 'admin.notifications-settings', type: 'link', path: '/settings/notifications', - icon: 'mdi:message-badge', - isMdiIcon: true + icon: 'mdi:message-badge' }, { id: 'repository_settings', @@ -673,7 +687,6 @@ export class MenuService { { name: 'asset-profile.asset-profiles', icon: 'mdi:alpha-a-box', - isMdiIcon: true, path: '/profiles/assetProfiles' } ] @@ -689,7 +702,6 @@ export class MenuService { { name: 'device-profile.device-profiles', icon: 'mdi:alpha-d-box', - isMdiIcon: true, path: '/profiles/deviceProfiles' }, { @@ -814,8 +826,7 @@ export class MenuService { name: 'alarm.alarms', type: 'link', path: '/alarms', - icon: 'mdi:alert-outline', - isMdiIcon: true + icon: 'mdi:alert-outline' }, { id: 'dashboards', @@ -874,7 +885,6 @@ export class MenuService { type: 'link', path: '/notification', icon: 'mdi:message-badge', - isMdiIcon: true, pages: [ { id: 'notification_inbox', diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index c310693b03..509be63ef7 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -130,7 +130,7 @@ export function isLiteralObject(value: any) { return (!!value) && (value.constructor === Object); } -export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { +export const formatValue = (value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined => { if (isDefinedAndNotNull(value) && isNumeric(value) && (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) { let formatted: string | number = Number(value); @@ -150,6 +150,16 @@ export function formatValue(value: any, dec?: number, units?: string, showZeroDe } } +export const formatNumberValue = (value: any, dec?: number): number | undefined => { + if (isDefinedAndNotNull(value) && isNumeric(value)) { + let formatted: string | number = Number(value); + if (isDefinedAndNotNull(dec)) { + formatted = formatted.toFixed(dec); + } + return Number(formatted); + } +} + export function objectValues(obj: any): any[] { return Object.keys(obj).map(e => obj[e]); } @@ -180,9 +190,7 @@ export function objToBase64(obj: any): string { } export function base64toString(b64Encoded: string): string { - return decodeURIComponent(atob(b64Encoded).split('').map((c) => { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }).join('')); + return decodeURIComponent(atob(b64Encoded).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')); } export function objToBase64URI(obj: any): string { @@ -190,9 +198,7 @@ export function objToBase64URI(obj: any): string { } export function base64toObj(b64Encoded: string): any { - const json = decodeURIComponent(atob(b64Encoded).split('').map((c) => { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }).join('')); + const json = decodeURIComponent(atob(b64Encoded).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')); return JSON.parse(json); } @@ -315,6 +321,8 @@ export const isEqual = (a: any, b: any): boolean => _.isEqual(a, b); export const isEmpty = (a: any): boolean => _.isEmpty(a); +export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path); + export const isEqualIgnoreUndefined = (a: any, b: any): boolean => { if (a === b) { return true; @@ -353,9 +361,7 @@ const SNAKE_CASE_REGEXP = /[A-Z]/g; export function snakeCase(name: string, separator: string): string { separator = separator || '_'; - return name.replace(SNAKE_CASE_REGEXP, (letter, pos) => { - return (pos ? separator : '') + letter.toLowerCase(); - }); + return name.replace(SNAKE_CASE_REGEXP, (letter, pos) => (pos ? separator : '') + letter.toLowerCase()); } export function getDescendantProp(obj: any, path: string): any { @@ -379,7 +385,7 @@ export function insertVariable(pattern: string, name: string, value: any): strin return result; } -export function createLabelFromDatasource(datasource: Datasource, pattern: string): string { +export const createLabelFromDatasource = (datasource: Datasource, pattern: string): string => { let label = pattern; if (!datasource) { return label; @@ -404,20 +410,23 @@ export function createLabelFromDatasource(datasource: Datasource, pattern: strin match = varsRegex.exec(pattern); } return label; -} +}; -export function formattedDataFormDatasourceData(input: DatasourceData[], dataIndex?: number): FormattedData[] { +export const hasDatasourceLabelsVariables = (pattern: string): boolean => varsRegex.test(pattern) !== null; + +export function formattedDataFormDatasourceData(input: DatasourceData[], dataIndex?: number, ts?: number): FormattedData[] { return _(input).groupBy(el => el.datasource.entityName + el.datasource.entityType) .values().value().map((entityArray, i) => { const datasource = entityArray[0].datasource; const obj = formattedDataFromDatasource(datasource, i); entityArray.filter(el => el.data.length).forEach(el => { const index = isDefined(dataIndex) ? dataIndex : el.data.length - 1; - if (!obj.hasOwnProperty(el.dataKey.label) || el.data[index][1] !== '') { - obj[el.dataKey.label] = el.data[index][1]; - obj[el.dataKey.label + '|ts'] = el.data[index][0]; + const dataSet = isDefined(ts) ? el.data.find(data => data[0] === ts) : el.data[index]; + if (dataSet !== undefined && (!obj.hasOwnProperty(el.dataKey.label) || dataSet[1] !== '')) { + obj[el.dataKey.label] = dataSet[1]; + obj[el.dataKey.label + '|ts'] = dataSet[0]; if (el.dataKey.label.toLowerCase() === 'type') { - obj.deviceType = el.data[index][1]; + obj.deviceType = dataSet[1]; } } }); @@ -692,7 +701,7 @@ export function getEntityDetailsPageURL(id: string, entityType: EntityType): str } export function parseHttpErrorMessage(errorResponse: HttpErrorResponse, - translate: TranslateService, responseType?: string): {message: string, timeout: number} { + translate: TranslateService, responseType?: string): {message: string; timeout: number} { let error = null; let errorMessage: string; let timeout = 0; @@ -774,3 +783,25 @@ export function genNextLabel(name: string, datasources: Datasource[]): string { } return label; } + +export const getOS = (): string => { + const userAgent = window.navigator.userAgent.toLowerCase(); + const macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos|mac_powerpc)/i; + const windowsPlatforms = /(win32|win64|windows|wince)/i; + const iosPlatforms = /(iphone|ipad|ipod|darwin|ios)/i; + let os = null; + + if (macosPlatforms.test(userAgent)) { + os = 'macos'; + } else if (iosPlatforms.test(userAgent)) { + os = 'ios'; + } else if (windowsPlatforms.test(userAgent)) { + os = 'windows'; + } else if (/android/.test(userAgent)) { + os = 'android'; + } else if (/linux/.test(userAgent)) { + os = 'linux'; + } + + return os; +}; diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 767633a3ce..d93a84ea51 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -83,15 +83,18 @@ import * as TbCore from '@core/public-api'; import * as TbShared from '@shared/public-api'; import * as TbHomeComponents from '@home/components/public-api'; -import * as MillisecondsToTimeStringPipe from '@shared/pipe/milliseconds-to-time-string.pipe'; +import * as DateAgoPipe from '@shared/pipe/date-ago.pipe'; import * as EnumToArrayPipe from '@shared/pipe/enum-to-array.pipe'; -import * as HighlightPipe from '@shared/pipe/highlight.pipe'; -import * as TruncatePipe from '@shared/pipe/truncate.pipe'; -import * as TbJsonPipe from '@shared/pipe/tbJson.pipe'; import * as FileSizePipe from '@shared/pipe/file-size.pipe'; +import * as HighlightPipe from '@shared/pipe/highlight.pipe'; +import * as KeyboardShortcutPipe from '@shared/pipe/keyboard-shortcut.pipe'; +import * as MillisecondsToTimeStringPipe from '@shared/pipe/milliseconds-to-time-string.pipe'; import * as NospacePipe from '@shared/pipe/nospace.pipe'; +import * as SafePipe from '@shared/pipe/safe.pipe'; import * as SelectableColumnsPipe from '@shared/pipe/selectable-columns.pipe'; -import * as KeyboardShortcutPipe from '@shared/pipe/keyboard-shortcut.pipe'; +import * as ShortNumberPipe from '@shared/pipe/short-number.pipe'; +import * as TbJsonPipe from '@shared/pipe/tbJson.pipe'; +import * as TruncatePipe from '@shared/pipe/truncate.pipe'; import * as coercion from '@shared/decorators/coercion'; import * as enumerable from '@shared/decorators/enumerable'; @@ -182,6 +185,8 @@ import * as ToggleHeaderComponent from '@shared/components/toggle-header.compone import * as ToggleSelectComponent from '@shared/components/toggle-select.component'; import * as UnitInputComponent from '@shared/components/unit-input.component'; import * as MaterialIconsComponent from '@shared/components/material-icons.component'; +import * as TbIconComponent from '@shared/components/icon.component'; +import * as HintTooltipIconComponent from '@shared/components/hint-tooltip-icon.component'; import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component'; import * as EntitiesTableComponent from '@home/components/entity/entities-table.component'; @@ -384,15 +389,18 @@ class ModulesMap implements IModulesMap { '@shared/public-api': TbShared, '@home/components/public-api': TbHomeComponents, - '@shared/pipe/milliseconds-to-time-string.pipe': MillisecondsToTimeStringPipe, + '@shared/pipe/date-ago.pipe': DateAgoPipe, '@shared/pipe/enum-to-array.pipe': EnumToArrayPipe, - '@shared/pipe/highlight.pipe': HighlightPipe, - '@shared/pipe/truncate.pipe': TruncatePipe, - '@shared/pipe/tbJson.pipe': TbJsonPipe, '@shared/pipe/file-size.pipe': FileSizePipe, + '@shared/pipe/highlight.pipe': HighlightPipe, + '@shared/pipe/keyboard-shortcut.pipe': KeyboardShortcutPipe, + '@shared/pipe/milliseconds-to-time-string.pipe': MillisecondsToTimeStringPipe, '@shared/pipe/nospace.pipe': NospacePipe, + '@shared/pipe/safe.pipe': SafePipe, '@shared/pipe/selectable-columns.pipe': SelectableColumnsPipe, - '@shared/pipe/keyboard-shortcut.pipe': KeyboardShortcutPipe, + '@shared/pipe/short-number.pipe': ShortNumberPipe, + '@shared/pipe/tbJson.pipe': TbJsonPipe, + '@shared/pipe/truncate.pipe': TruncatePipe, '@shared/decorators/coercion': coercion, '@shared/decorators/enumerable': enumerable, @@ -484,6 +492,8 @@ class ModulesMap implements IModulesMap { '@shared/components/toggle-select.component': ToggleSelectComponent, '@shared/components/unit-input.component': UnitInputComponent, '@shared/components/material-icons.component': MaterialIconsComponent, + '@shared/components/icon.component': TbIconComponent, + '@shared/components/hint-tooltip-icon.component': HintTooltipIconComponent, '@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent, '@home/components/entity/entities-table.component': EntitiesTableComponent, diff --git a/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts b/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts index 357db25e67..a89f467e1e 100644 --- a/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts +++ b/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts @@ -38,13 +38,8 @@ export class WidgetEditorDashboardResolver implements Resolve { resolve(route: ActivatedRouteSnapshot): Dashboard { const editWidgetInfo = this.utils.editWidgetInfo; const widget: Widget = { - isSystemType: true, - bundleAlias: 'customWidgetBundle', - typeAlias: 'customWidget', + typeFullFqn: 'system.customWidget', type: editWidgetInfo.type, - title: 'My widget', - image: null, - description: null, sizeX: editWidgetInfo.sizeX * 2, sizeY: editWidgetInfo.sizeY * 2, row: 2, diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html index c5ed4fe52a..3a1be706ed 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html @@ -55,8 +55,8 @@ -
-
+
+
alarm.alarm-status-list
@@ -64,7 +64,7 @@
-
+
alarm.alarm-severity-list
@@ -72,24 +72,16 @@
-
-
alarm.alarm-type-list
- - - - {{type}} - cancel - - - - +
+
alarm.alarm-type-list
+ +
-
+
alarm.assignee
('AlarmFilterConfigData'); @@ -91,8 +92,6 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal panelMode = false; - readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; - alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACK, @@ -113,7 +112,10 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal panelResult: AlarmFilterConfig = null; + entityType = EntityType; + private alarmFilterConfig: AlarmFilterConfig; + private resizeWindows: Subscription; private propagateChange = (_: any) => {}; @@ -198,14 +200,9 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal minWidth: '' }); config.hasBackdrop = true; - const connectedPosition: ConnectedPosition = { - originX: 'start', - originY: 'bottom', - overlayX: 'start', - overlayY: 'top' - }; - config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement) - .withPositions([connectedPosition]); + config.positionStrategy = this.overlay.position() + .flexibleConnectedTo(this.nativeElement) + .withPositions([POSITION_MAP.bottomLeft]); this.alarmFilterOverlayRef = this.overlay.create(config); this.alarmFilterOverlayRef.backdropClick().subscribe(() => { @@ -213,6 +210,9 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal }); this.alarmFilterOverlayRef.attach(new TemplatePortal(this.alarmFilterPanel, this.viewContainerRef)); + this.resizeWindows = fromEvent(window, 'resize').subscribe(() => { + this.alarmFilterOverlayRef.updatePosition(); + }); } cancel() { @@ -220,6 +220,7 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal if (this.overlayRef) { this.overlayRef.dispose(); } else { + this.resizeWindows.unsubscribe(); this.alarmFilterOverlayRef.dispose(); } } @@ -233,6 +234,7 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal if (this.overlayRef) { this.overlayRef.dispose(); } else { + this.resizeWindows.unsubscribe(); this.alarmFilterOverlayRef.dispose(); } } @@ -256,40 +258,6 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal } } - public alarmTypeList(): string[] { - return this.alarmFilterConfigForm.get('typeList').value; - } - - public removeAlarmType(type: string): void { - const types: string[] = this.alarmFilterConfigForm.get('typeList').value; - const index = types.indexOf(type); - if (index >= 0) { - types.splice(index, 1); - this.alarmFilterConfigForm.get('typeList').setValue(types); - this.alarmFilterConfigForm.get('typeList').markAsDirty(); - } - } - - public addAlarmType(event: MatChipInputEvent): void { - const input = event.chipInput.inputElement; - const value = event.value; - - let types: string[] = this.alarmFilterConfigForm.get('typeList').value; - - if ((value || '').trim()) { - if (!types) { - types = []; - } - types.push(value.trim()); - this.alarmFilterConfigForm.get('typeList').setValue(types); - this.alarmFilterConfigForm.get('typeList').markAsDirty(); - } - - if (input) { - input.value = ''; - } - } - private updateAlarmConfigForm(alarmFilterConfig?: AlarmFilterConfig) { this.alarmFilterConfigForm.patchValue({ statusList: alarmFilterConfig?.statusList, diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index 28e64738a8..15e3fb1952 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -26,17 +26,17 @@ import { DatePipe } from '@angular/common'; import { Direction } from '@shared/models/page/sort-order'; import { MatDialog } from '@angular/material/dialog'; import { TimePageLink } from '@shared/models/page/page-link'; -import { Observable } from 'rxjs'; +import { forkJoin, Observable } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { EntityId } from '@shared/models/id/entity-id'; import { AlarmInfo, - AlarmQuery, AlarmQueryV2, AlarmSearchStatus, alarmSeverityColors, alarmSeverityTranslations, AlarmsMode, + AlarmStatus, alarmStatusTranslations } from '@app/shared/models/alarm.models'; import { AlarmService } from '@app/core/http/alarm.service'; @@ -46,7 +46,7 @@ import { AlarmDetailsDialogComponent, AlarmDetailsDialogData } from '@home/components/alarm/alarm-details-dialog.component'; -import { DAY, forAllTimeInterval, historyInterval } from '@shared/models/time/time.models'; +import { forAllTimeInterval } from '@shared/models/time/time.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; @@ -93,7 +93,7 @@ export class AlarmTableConfig extends EntityTableConfig this.pageMode = pageMode; this.defaultTimewindowInterval = forAllTimeInterval(); this.detailsPanelEnabled = false; - this.selectionEnabled = false; + this.selectionEnabled = true; this.searchEnabled = true; this.addEnabled = false; this.entitiesDeleteEnabled = false; @@ -155,6 +155,27 @@ export class AlarmTableConfig extends EntityTableConfig onAction: ($event, entity) => this.showAlarmDetails(entity) } ); + + this.groupActionDescriptors.push( + { + name: this.translate.instant('alarm.acknowledge'), + icon: 'done', + isEnabled: true, + onAction: ($event, entities) => this.ackAlarms($event, entities) + }, + { + name: this.translate.instant('alarm.clear'), + icon: 'clear', + isEnabled: true, + onAction: ($event, entities) => this.clearAlarms($event, entities) + }, + { + name: this.translate.instant('alarm.delete'), + icon: 'delete', + isEnabled: true, + onAction: ($event, entities) => this.deleteAlarms($event, entities) + } + ) } fetchAlarms(pageLink: TimePageLink): Observable> { @@ -294,4 +315,103 @@ export class AlarmTableConfig extends EntityTableConfig }); } + ackAlarms($event: Event, alarms: Array) { + if ($event) { + $event.stopPropagation(); + } + const unacknowledgedAlarms = alarms.filter(alarm => { + return alarm.status === AlarmStatus.CLEARED_UNACK || alarm.status === AlarmStatus.ACTIVE_UNACK; + }) + let title = ''; + let content = ''; + if (!unacknowledgedAlarms.length) { + title = this.translate.instant('alarm.selected-alarms', {count: alarms.length}); + content = this.translate.instant('alarm.selected-alarms-are-acknowledged'); + this.dialogService.alert( + title, + content).subscribe(); + } else { + title = this.translate.instant('alarm.aknowledge-alarms-title', {count: unacknowledgedAlarms.length}); + content = this.translate.instant('alarm.aknowledge-alarms-text', {count: unacknowledgedAlarms.length}); + this.dialogService.confirm( + title, + content, + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + const tasks: Observable[] = []; + for (const alarm of unacknowledgedAlarms) { + tasks.push(this.alarmService.ackAlarm(alarm.id.id)); + } + forkJoin(tasks).subscribe(() => { + this.updateData(); + }); + } + }); + } + } + + clearAlarms($event: Event, alarms: Array) { + if ($event) { + $event.stopPropagation(); + } + const activeAlarms = alarms.filter(alarm => { + return alarm.status === AlarmStatus.ACTIVE_ACK || alarm.status === AlarmStatus.ACTIVE_UNACK; + }) + let title = ''; + let content = ''; + if (!activeAlarms.length) { + title = this.translate.instant('alarm.selected-alarms', {count: alarms.length}); + content = this.translate.instant('alarm.selected-alarms-are-cleared'); + this.dialogService.alert( + title, + content + ).subscribe(); + } else { + title = this.translate.instant('alarm.clear-alarms-title', {count: activeAlarms.length}); + content = this.translate.instant('alarm.clear-alarms-text', {count: activeAlarms.length}); + this.dialogService.confirm( + title, + content, + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + const tasks: Observable[] = []; + for (const alarm of activeAlarms) { + tasks.push(this.alarmService.clearAlarm(alarm.id.id)); + } + forkJoin(tasks).subscribe(() => { + this.updateData(); + }); + } + }); + } + } + + deleteAlarms($event: Event, alarms: Array) { + if ($event) { + $event.stopPropagation(); + } + const title = this.translate.instant('alarm.delete-alarms-title', {count: alarms.length}); + const content = this.translate.instant('alarm.delete-alarms-text', {count: alarms.length}); + this.dialogService.confirm( + title, + content, + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + const tasks: Observable[] = []; + for (const alarm of alarms) { + tasks.push(this.alarmService.deleteAlarm(alarm.id.id)); + } + forkJoin(tasks).subscribe(() => { + this.updateData(); + }); + } + }); + } + } diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html index c48299986e..17e31854cc 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -17,7 +17,7 @@ -->
-

{{ 'attribute.add' | translate }}

+

{{ title | translate }}

diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss index 831762d1e4..b33dfdbb20 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss @@ -104,7 +104,7 @@ } mat-cell.tb-value-cell { cursor: pointer; - mat-icon { + .mat-icon { height: 24px; width: 24px; font-size: 24px; diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index e9d67a3fdd..872e124f3a 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -23,6 +23,7 @@ import { Injector, Input, NgZone, + OnDestroy, OnInit, StaticProvider, ViewChild, @@ -38,8 +39,8 @@ import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { DialogService } from '@core/services/dialog.service'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; -import { fromEvent, merge } from 'rxjs'; -import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { merge, Subject } from 'rxjs'; +import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'; import { EntityId } from '@shared/models/id/entity-id'; import { AttributeData, @@ -49,6 +50,7 @@ import { LatestTelemetry, TelemetryType, telemetryTypeTranslations, + TimeseriesDeleteStrategy, toTelemetryType } from '@shared/models/telemetry/telemetry.models'; import { AttributeDatasource } from '@home/models/datasource/attribute-datasource'; @@ -82,10 +84,16 @@ import { AddWidgetToDashboardDialogComponent, AddWidgetToDashboardDialogData } from '@home/components/attribute/add-widget-to-dashboard-dialog.component'; -import { deepClone } from '@core/utils'; +import { deepClone, isUndefinedOrNull } from '@core/utils'; import { Filters } from '@shared/models/query/query.models'; import { hidePageSizePixelValue } from '@shared/models/constants'; import { ResizeObserver } from '@juggle/resize-observer'; +import { + DELETE_TIMESERIES_PANEL_DATA, + DeleteTimeseriesPanelComponent, + DeleteTimeseriesPanelData +} from '@home/components/attribute/delete-timeseries-panel.component'; +import { FormBuilder } from '@angular/forms'; @Component({ @@ -94,12 +102,13 @@ import { ResizeObserver } from '@juggle/resize-observer'; styleUrls: ['./attribute-table.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class AttributeTableComponent extends PageComponent implements AfterViewInit, OnInit { +export class AttributeTableComponent extends PageComponent implements AfterViewInit, OnInit, OnDestroy { telemetryTypeTranslationsMap = telemetryTypeTranslations; isClientSideTelemetryTypeMap = isClientSideTelemetryType; latestTelemetryTypes = LatestTelemetry; + attributeScopeTypes = AttributeScope; mode: 'default' | 'widget' = 'default'; @@ -178,6 +187,10 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; + textSearch = this.fb.control('', {nonNullable: true}); + + private destroy$ = new Subject(); + constructor(protected store: Store, private attributeService: AttributeService, private telemetryWsService: TelemetryWebsocketService, @@ -192,7 +205,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI private widgetService: WidgetService, private zone: NgZone, private cd: ChangeDetectorRef, - private elementRef: ElementRef) { + private elementRef: ElementRef, + private fb: FormBuilder) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC }; @@ -215,6 +229,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI if (this.widgetResize$) { this.widgetResize$.disconnect(); } + this.destroy$.next(); + this.destroy$.complete(); } attributeScopeChanged(attributeScope: TelemetryType) { @@ -225,25 +241,21 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI } ngAfterViewInit() { + this.textSearch.valueChanges.pipe( + debounceTime(150), + distinctUntilChanged((prev, current) => (this.pageLink.textSearch ?? '') === current.trim()), + takeUntil(this.destroy$) + ).subscribe((value) => { + this.paginator.pageIndex = 0; + this.pageLink.textSearch = value.trim(); + this.updateData(); + }); + + this.sort.sortChange.pipe(takeUntil(this.destroy$)).subscribe(() => this.paginator.pageIndex = 0); - fromEvent(this.searchInputField.nativeElement, 'keyup') - .pipe( - debounceTime(150), - distinctUntilChanged(), - tap(() => { - this.paginator.pageIndex = 0; - this.updateData(); - }) - ) - .subscribe(); - - this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); - - merge(this.sort.sortChange, this.paginator.page) - .pipe( - tap(() => this.updateData()) - ) - .subscribe(); + merge(this.sort.sortChange, this.paginator.page).pipe( + takeUntil(this.destroy$) + ).subscribe(() => this.updateData()); this.viewsInited = true; if (this.activeValue && this.entityIdValue) { @@ -261,7 +273,6 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI enterFilterMode() { this.textSearchMode = true; - this.pageLink.textSearch = ''; setTimeout(() => { this.searchInputField.nativeElement.focus(); this.searchInputField.nativeElement.setSelectionRange(0, 0); @@ -270,9 +281,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI exitFilterMode() { this.textSearchMode = false; - this.pageLink.textSearch = null; - this.paginator.pageIndex = 0; - this.updateData(); + this.textSearch.reset(); } resetSortAndFilter(update: boolean = true) { @@ -289,6 +298,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI this.selectedWidgetsBundleAlias = null; this.attributeScope = this.defaultAttributeScope; this.pageLink.textSearch = null; + this.textSearch.reset('', {emitEvent: false}); if (this.viewsInited) { this.paginator.pageIndex = 0; const sortable = this.sort.sortables.get('key'); @@ -313,7 +323,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { entityId: this.entityIdValue, - attributeScope: this.attributeScope as AttributeScope + attributeScope: this.attributeScope } }).afterClosed().subscribe( (res) => { @@ -378,6 +388,82 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI }); } + deleteTimeseries($event: Event, telemetry?: AttributeData) { + if ($event) { + $event.stopPropagation(); + } + const isMultipleDeletion = isUndefinedOrNull(telemetry) && this.dataSource.selection.selected.length > 1; + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig({ + panelClass: 'tb-filter-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + maxWidth: 488, + width: '100%' + }); + const connectedPosition: ConnectedPosition = { + originX: 'start', + originY: 'top', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + + const providers: StaticProvider[] = [ + { + provide: DELETE_TIMESERIES_PANEL_DATA, + useValue: { + isMultipleDeletion + } as DeleteTimeseriesPanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + const componentRef = overlayRef.attach(new ComponentPortal(DeleteTimeseriesPanelComponent, + this.viewContainerRef, injector)); + componentRef.onDestroy(() => { + if (componentRef.instance.result !== null) { + const result = componentRef.instance.result; + const deleteTimeseries = telemetry ? [telemetry]: this.dataSource.selection.selected; + let deleteAllDataForKeys = false; + let rewriteLatestIfDeleted = false; + let startTs = null; + let endTs = null; + let deleteLatest = true; + switch (result.strategy) { + case TimeseriesDeleteStrategy.DELETE_ALL_DATA: + deleteAllDataForKeys = true; + break; + case TimeseriesDeleteStrategy.DELETE_ALL_DATA_EXCEPT_LATEST_VALUE: + deleteAllDataForKeys = true; + deleteLatest = false; + break; + case TimeseriesDeleteStrategy.DELETE_LATEST_VALUE: + rewriteLatestIfDeleted = result.rewriteLatest; + startTs = deleteTimeseries[0].lastUpdateTs; + endTs = startTs + 1; + break; + case TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD: + startTs = result.startDateTime.getTime(); + endTs = result.endDateTime.getTime(); + rewriteLatestIfDeleted = result.rewriteLatest; + break; + } + this.attributeService.deleteEntityTimeseries(this.entityIdValue, deleteTimeseries, deleteAllDataForKeys, + startTs, endTs, rewriteLatestIfDeleted, deleteLatest) + .subscribe(() => this.reloadAttributes()); + } + }); + } + deleteAttributes($event: Event) { if ($event) { $event.stopPropagation(); @@ -402,6 +488,14 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI } } + deleteTelemetry($event: Event) { + if (this.attributeScope === this.latestTelemetryTypes.LATEST_TELEMETRY) { + this.deleteTimeseries($event); + } else { + this.deleteAttributes($event); + } + } + enterWidgetMode() { this.mode = 'widget'; this.widgetsList = []; @@ -421,9 +515,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI // @ts-ignore const stateController: IStateController = { - getStateParams(): StateParams { - return {}; - } + getStateParams: (): StateParams => ({}) }; const filters: Filters = {}; @@ -475,9 +567,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI this.widgetsCarouselIndex = 0; if (widgetsBundle) { this.widgetsLoaded = false; - const bundleAlias = widgetsBundle.alias; - const isSystem = widgetsBundle.tenantId.id === NULL_UUID; - this.widgetService.getBundleWidgetTypes(bundleAlias, isSystem).subscribe( + this.widgetService.getBundleWidgetTypes(widgetsBundle.id.id).subscribe( (widgetTypes) => { widgetTypes = widgetTypes.sort((a, b) => { let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]); @@ -493,11 +583,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI const sizeY = widgetInfo.sizeY * 2; const col = Math.floor(Math.max(0, (20 - sizeX) / 2)); const widget: Widget = { - isSystemType: isSystem, - bundleAlias, - typeAlias: widgetInfo.alias, + typeFullFqn: widgetInfo.fullFqn, type: widgetInfo.type, - title: widgetInfo.widgetName, sizeX, sizeY, row: 0, diff --git a/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html new file mode 100644 index 0000000000..5778fc6805 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html @@ -0,0 +1,68 @@ + + + +

{{ "attribute.delete-timeseries.delete-strategy" | translate }}

+ + +
+ + + attribute.delete-timeseries.strategy + + + {{ strategiesTranslationsMap.get(strategy) | translate }} + + + +
+ + attribute.delete-timeseries.start-time + + + + + + attribute.delete-timeseries.ends-on + + + + +
+ + {{ "attribute.delete-timeseries.rewrite-latest-value" | translate }} + + +
+ + + +
diff --git a/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.scss b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.scss new file mode 100644 index 0000000000..c9a0e527f7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.scss @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2023 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'; + +:host { + width: 100%; + + .mat-toolbar { + background: none; + } + + .tb-form-settings { + flex-direction: column; + gap: 16px; + padding-top: 0; + } + + .tb-select-interval { + display: flex; + flex-direction: row; + gap: 16px; + @media #{$mat-xs} { + flex-direction: column; + gap: 0; + } + } + + .tb-slide-toggle { + margin-bottom: 8px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.ts b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.ts new file mode 100644 index 0000000000..94c66b3734 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.ts @@ -0,0 +1,151 @@ +/// +/// Copyright © 2016-2023 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, Inject, InjectionToken, OnDestroy, OnInit } from '@angular/core'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { + TimeseriesDeleteStrategy, + timeseriesDeleteStrategyTranslations +} from '@shared/models/telemetry/telemetry.models'; +import { MINUTE } from '@shared/models/time/time.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +export const DELETE_TIMESERIES_PANEL_DATA = new InjectionToken('DeleteTimeseriesPanelData'); + +export interface DeleteTimeseriesPanelData { + isMultipleDeletion: boolean; +} + +export interface DeleteTimeseriesPanelResult { + strategy: TimeseriesDeleteStrategy; + startDateTime: Date; + endDateTime: Date; + rewriteLatest: boolean; +} + +@Component({ + selector: 'tb-delete-timeseries-panel', + templateUrl: './delete-timeseries-panel.component.html', + styleUrls: ['./delete-timeseries-panel.component.scss'] +}) +export class DeleteTimeseriesPanelComponent implements OnInit, OnDestroy { + + deleteTimeseriesFormGroup: FormGroup; + + result: DeleteTimeseriesPanelResult = null; + + strategiesTranslationsMap = timeseriesDeleteStrategyTranslations; + + private multipleDeletionStrategies = new Set([ + TimeseriesDeleteStrategy.DELETE_ALL_DATA, + TimeseriesDeleteStrategy.DELETE_ALL_DATA_EXCEPT_LATEST_VALUE + ]); + + private destroy$ = new Subject(); + + constructor(@Inject(DELETE_TIMESERIES_PANEL_DATA) private data: DeleteTimeseriesPanelData, + private overlayRef: OverlayRef, + private fb: FormBuilder) { } + + ngOnInit(): void { + const today = new Date(); + if (this.data.isMultipleDeletion) { + this.strategiesTranslationsMap = new Map(Array.from(this.strategiesTranslationsMap) + .filter(([strategy]) => this.multipleDeletionStrategies.has(strategy))) + } + this.deleteTimeseriesFormGroup = this.fb.group({ + strategy: [TimeseriesDeleteStrategy.DELETE_ALL_DATA], + startDateTime: [ + { value: new Date(today.getFullYear(), today.getMonth() - 1, today.getDate()), disabled: true }, + Validators.required + ], + endDateTime: [{ value: today, disabled: true }, Validators.required], + rewriteLatest: [true] + }) + + this.deleteTimeseriesFormGroup.get('strategy').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value === TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD) { + this.deleteTimeseriesFormGroup.get('startDateTime').enable({onlySelf: true, emitEvent: false}); + this.deleteTimeseriesFormGroup.get('endDateTime').enable({onlySelf: true, emitEvent: false}); + } else { + this.deleteTimeseriesFormGroup.get('startDateTime').disable({onlySelf: true, emitEvent: false}); + this.deleteTimeseriesFormGroup.get('endDateTime').disable({onlySelf: true, emitEvent: false}); + } + }) + this.deleteTimeseriesFormGroup.get('startDateTime').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => this.onStartDateTimeChange(value)); + this.deleteTimeseriesFormGroup.get('endDateTime').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => this.onEndDateTimeChange(value)); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + delete(): void { + if (this.deleteTimeseriesFormGroup.valid) { + this.result = this.deleteTimeseriesFormGroup.value; + this.overlayRef.dispose(); + } else { + this.deleteTimeseriesFormGroup.markAllAsTouched(); + } + } + + cancel(): void { + this.overlayRef.dispose(); + } + + isPeriodStrategy(): boolean { + return this.deleteTimeseriesFormGroup.get('strategy').value === TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD; + } + + isDeleteLatestStrategy(): boolean { + return this.deleteTimeseriesFormGroup.get('strategy').value === TimeseriesDeleteStrategy.DELETE_LATEST_VALUE; + } + + private onStartDateTimeChange(newStartDateTime: Date) { + if (newStartDateTime) { + const endDateTimeTs = this.deleteTimeseriesFormGroup.get('endDateTime').value.getTime(); + if (newStartDateTime.getTime() >= endDateTimeTs) { + this.deleteTimeseriesFormGroup.get('startDateTime') + .patchValue(new Date(endDateTimeTs - MINUTE), {onlySelf: true, emitEvent: false}); + } else { + this.deleteTimeseriesFormGroup.get('startDateTime') + .patchValue(newStartDateTime, {onlySelf: true, emitEvent: false}); + } + } + } + + private onEndDateTimeChange(newEndDateTime: Date) { + if (newEndDateTime) { + const startDateTimeTs = this.deleteTimeseriesFormGroup.get('startDateTime').value.getTime(); + if (newEndDateTime.getTime() <= startDateTimeTs) { + this.deleteTimeseriesFormGroup.get('endDateTime') + .patchValue(new Date(startDateTimeTs + MINUTE), {onlySelf: true, emitEvent: false}); + } else { + this.deleteTimeseriesFormGroup.get('endDateTime') + .patchValue(newEndDateTime, {onlySelf: true, emitEvent: false}); + } + } + } +} 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 ad08c0c89e..05ea8a84a1 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 @@ -15,16 +15,19 @@ limitations under the License. --> -
+ -

widget.add

- : {{data.widgetInfo.widgetName}} -
- - {{ 'widget.basic-mode' | translate }} - {{ 'widget.advanced-mode' | translate }} - -
+
+

{{'widget.add' | translate}}:

+ {{data.widgetInfo.widgetName}} +
+ + {{ 'widget.basic-mode' | translate }} + {{ 'widget.advanced-mode' | translate }} + +
+
- - -
-
- - - -
- -
-
-
+
+ + + +
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.scss new file mode 100644 index 0000000000..6f7eb4bd3b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.scss @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2023 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-add-widget-dialog { + .mat-mdc-dialog-content { + padding: 0; + position: relative; + } + @media #{$mat-gt-xs} { + width: 900px; + .mat-mdc-dialog-content { + height: 900px; + } + } + .tb-add-widget-toolbar { + flex: 1; + min-width: 0; + display: flex; + flex-direction: row; + place-content: center flex-start; + align-items: center; + gap: 8px; + } +} 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 ada22bba94..ffc453c42a 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 @@ -14,12 +14,12 @@ /// limitations under the License. /// -import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { Component, Inject, OnInit, SkipSelf, ViewChild, ViewEncapsulation } from '@angular/core'; import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { Widget, WidgetConfigMode, widgetTypesData } from '@shared/models/widget.models'; @@ -28,6 +28,7 @@ import { IAliasController, IStateController } from '@core/api/widget-api.models' import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models'; import { isDefined, isDefinedAndNotNull, isString } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; export interface AddWidgetDialogData { dashboard: Dashboard; @@ -41,12 +42,14 @@ export interface AddWidgetDialogData { selector: 'tb-add-widget-dialog', templateUrl: './add-widget-dialog.component.html', providers: [/*{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}*/], - styleUrls: [] + styleUrls: ['./add-widget-dialog.component.scss'], + encapsulation: ViewEncapsulation.None }) export class AddWidgetDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { - widgetConfigModes = WidgetConfigMode; + @ViewChild('widgetConfigComponent') + widgetConfigComponent: WidgetConfigComponent; widgetFormGroup: UntypedFormGroup; @@ -59,17 +62,17 @@ export class AddWidgetDialogComponent extends DialogComponent
-
+
Top % @@ -53,14 +53,14 @@
-
+
+
-
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 6432a3f234..9b5a742ecf 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 @@ -25,8 +25,7 @@ [toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()">
-
+
@@ -74,8 +73,7 @@
-
+
+ + + + @@ -360,7 +367,7 @@ [isReadOnly]="true" (closeDetails)="onEditWidgetClosed()">
- + {{ 'widget.basic-mode' | translate }} {{ 'widget.advanced-mode' | translate }} @@ -379,8 +386,9 @@
-
+
@@ -412,6 +421,23 @@ matTooltipPosition="above"> filter_list + + {{ 'widgets-bundle.widgets-bundles' | translate }} + {{ 'widget.all-widgets' | translate }} + + + {{ 'widget.all' | translate }} + {{ 'widget.actual' | translate }} + {{ 'widget.deprecated' | translate }} +
{ const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo); if (!config.title) { config.title = 'New ' + widgetTypeInfo.widgetName; } let newWidget: Widget = { - isSystemType: widget.isSystemType, - bundleAlias: widget.bundleAlias, - typeAlias: widgetTypeInfo.alias, + typeFullFqn: widgetTypeInfo.fullFqn, type: widgetTypeInfo.type, - title: 'New widget', - image: null, - description: null, sizeX: widgetTypeInfo.sizeX, sizeY: widgetTypeInfo.sizeY, config, @@ -1177,6 +1176,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC Widget>(AddWidgetDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + maxWidth: '95vw', data: { dashboard: this.dashboard, aliasController: this.dashboardCtx.aliasController, diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss index 0d9ade9bf6..e43132c761 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss @@ -126,6 +126,7 @@ tb-dashboard-toolbar { @media #{$mat-lt-md} { height: $mobile-toolbar-height; max-height: $mobile-toolbar-height; + padding: 0 8px !important; } .close-action { @@ -150,8 +151,44 @@ tb-dashboard-toolbar { .tb-dashboard-action-panel { min-width: 0; height: $half-mobile-toolbar-height; + flex: 1 0 auto; + display: flex; + flex-direction: row-reverse; + place-content: center space-between; + align-items: center; + &.tb-left-panel { + flex: 1 1 auto; + } + + @media #{$mat-lt-md} { + padding-left: 12px; + } + + @media #{$mat-xs} { + gap: 3px; + padding-left: 0; + &.tb-left-panel { + padding-left: 12px; + } + } + + @media #{$mat-sm} { + gap: 6px; + } + + @media #{$mat-md} { + gap: 6px; + } + + @media #{$mat-gt-md} { + gap: 12px; + } @media #{$mat-gt-sm} { + place-content: center flex-start; + &.tb-left-panel { + place-content: center flex-end; + } height: 46px; } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html index 076ffa8db7..44882ab889 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html @@ -16,15 +16,23 @@ -->
-
-
-
+ +
+ +
+
+ + + + +
+
{{ widget.title }}
- {{widget.title}} + {{widget.title}}
widget.deprecated
{{ 'widget.' + widget.type | translate }} {{ widget.description }} @@ -33,22 +41,22 @@
- -
+ + +
{{ 'widget.loading-widgets' | translate }} - -
- + +
+
+ widgets-bundle.empty - - -
+ class="mat-headline-5 tb-absolute-fill">{{(selectWidgetMode === 'bundles' ? 'widgets-bundle.empty' : 'widget.no-widgets-text') | translate}} +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss index a82f9c2f8b..3a290e53e5 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss @@ -54,6 +54,10 @@ font-size: 20px; line-height: normal; margin-bottom: 8px; + .tb-deprecated { + font-size: 14px; + color: rgba(209, 39, 48, 0.87); + } } .mat-mdc-card-subtitle { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts index bfe1c492ec..8a359de66c 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts @@ -19,11 +19,17 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { IAliasController } from '@core/api/widget-api.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { WidgetService } from '@core/http/widget.service'; -import { WidgetInfo, widgetType } from '@shared/models/widget.models'; -import { distinctUntilChanged, map, publishReplay, refCount, share, switchMap, tap } from 'rxjs/operators'; -import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs'; +import { fullWidgetTypeFqn, WidgetInfo, widgetType, WidgetTypeInfo } from '@shared/models/widget.models'; +import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Observable, of, ReplaySubject } from 'rxjs'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { isDefinedAndNotNull } from '@core/utils'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; + +type widgetsListMode = 'all' | 'actual' | 'deprecated'; + +type selectWidgetMode = 'bundles' | 'allWidgets'; @Component({ selector: 'tb-dashboard-widget-select', @@ -34,10 +40,14 @@ export class DashboardWidgetSelectComponent implements OnInit { private search$ = new BehaviorSubject(''); private filterWidgetTypes$ = new BehaviorSubject>(null); + private widgetsListMode$ = new BehaviorSubject('actual'); + private selectWidgetMode$ = new BehaviorSubject('bundles'); private widgetsInfo: Observable>; private widgetsBundleValue: WidgetsBundle; widgetTypes = new Set(); + hasDeprecated = false; + allWidgets$: Observable>; widgets$: Observable>; loadingWidgetsSubject: BehaviorSubject = new BehaviorSubject(false); loadingWidgets$ = this.loadingWidgetsSubject.pipe( @@ -54,8 +64,10 @@ export class DashboardWidgetSelectComponent implements OnInit { this.widgetsBundleValue = widgetBundle; if (widgetBundle === null) { this.widgetTypes.clear(); + this.hasDeprecated = false; } this.filterWidgetTypes$.next(null); + this.widgetsListMode$.next('actual'); this.widgetsInfo = null; } } @@ -81,6 +93,24 @@ export class DashboardWidgetSelectComponent implements OnInit { return this.filterWidgetTypes$.value; } + @Input() + set selectWidgetMode(mode: selectWidgetMode) { + this.selectWidgetMode$.next(mode); + } + + get selectWidgetMode(): selectWidgetMode { + return this.selectWidgetMode$.value; + } + + @Input() + set widgetsListMode(mode: widgetsListMode) { + this.widgetsListMode$.next(mode); + } + + get widgetsListMode(): widgetsListMode { + return this.widgetsListMode$.value; + } + @Output() widgetSelected: EventEmitter = new EventEmitter(); @@ -90,13 +120,20 @@ export class DashboardWidgetSelectComponent implements OnInit { constructor(private widgetsService: WidgetService, private sanitizer: DomSanitizer, private cd: ChangeDetectorRef) { - this.widgetsBundles$ = this.search$.asObservable().pipe( - distinctUntilChanged(), - switchMap(search => this.fetchWidgetBundle(search)) + this.widgetsBundles$ = combineLatest([this.search$.asObservable(), this.selectWidgetMode$.asObservable()]).pipe( + distinctUntilChanged((oldValue, newValue) => JSON.stringify(oldValue) === JSON.stringify(newValue)), + switchMap(search => this.fetchWidgetBundle(...search)) ); - this.widgets$ = combineLatest([this.search$.asObservable(), this.filterWidgetTypes$.asObservable()]).pipe( + this.allWidgets$ = combineLatest([this.search$.asObservable().pipe( + debounceTime(150) + ), this.selectWidgetMode$.asObservable()]).pipe( + distinctUntilChanged((oldValue, newValue) => JSON.stringify(oldValue) === JSON.stringify(newValue)), + switchMap(search => this.fetchAllWidgets(...search)), + share({ connector: () => new ReplaySubject(1), resetOnError: false, resetOnComplete: false, resetOnRefCountZero: false }) + ); + this.widgets$ = combineLatest([this.search$.asObservable(), this.filterWidgetTypes$.asObservable(), this.widgetsListMode$]).pipe( distinctUntilChanged((oldValue, newValue) => JSON.stringify(oldValue) === JSON.stringify(newValue)), - switchMap(search => this.fetchWidget(...search)) + switchMap(search => this.fetchWidgets(...search)) ); } @@ -106,29 +143,19 @@ export class DashboardWidgetSelectComponent implements OnInit { private getWidgets(): Observable> { if (!this.widgetsInfo) { if (this.widgetsBundle !== null) { - const bundleAlias = this.widgetsBundle.alias; - const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID; this.loadingWidgetsSubject.next(true); - this.widgetsInfo = this.widgetsService.getBundleWidgetTypeInfos(bundleAlias, isSystem).pipe( + this.widgetsInfo = this.widgetsService.getBundleWidgetTypeInfos(this.widgetsBundle.id.id).pipe( map(widgets => { - widgets = widgets.sort((a, b) => b.createdTime - a.createdTime); const widgetTypes = new Set(); + const hasDeprecated = widgets.some(w => w.deprecated); const widgetInfos = widgets.map((widgetTypeInfo) => { widgetTypes.add(widgetTypeInfo.widgetType); - const widget: WidgetInfo = { - isSystemType: isSystem, - bundleAlias, - typeAlias: widgetTypeInfo.alias, - type: widgetTypeInfo.widgetType, - title: widgetTypeInfo.name, - image: widgetTypeInfo.image, - description: widgetTypeInfo.description - }; - return widget; + return this.toWidgetInfo(widgetTypeInfo); } ); setTimeout(() => { this.widgetTypes = widgetTypes; + this.hasDeprecated = hasDeprecated; this.cd.markForCheck(); }); return widgetInfos; @@ -136,8 +163,7 @@ export class DashboardWidgetSelectComponent implements OnInit { tap(() => { this.loadingWidgetsSubject.next(false); }), - publishReplay(1), - refCount() + share({ connector: () => new ReplaySubject(1), resetOnError: false, resetOnComplete: false, resetOnRefCountZero: false }) ); } else { this.widgetsInfo = of([]); @@ -168,27 +194,32 @@ export class DashboardWidgetSelectComponent implements OnInit { return '/assets/widget-preview-empty.svg'; } - private getWidgetsBundle(): Observable> { + private getWidgetsBundles(): Observable> { return this.widgetsService.getAllWidgetsBundles().pipe( tap(() => this.loadingWidgetBundlesSubject.next(false)), - publishReplay(1), - refCount() + share({ connector: () => new ReplaySubject(1), resetOnError: false, resetOnComplete: false, resetOnRefCountZero: false }) ); } - private fetchWidgetBundle(search: string): Observable> { - return this.getWidgetsBundle().pipe( - map(bundles => search ? bundles.filter( - bundle => ( - bundle.title?.toLowerCase().includes(search.toLowerCase()) || - bundle.description?.toLowerCase().includes(search.toLowerCase()) - )) : bundles - ) - ); + private fetchWidgetBundle(search: string, mode: selectWidgetMode): Observable> { + if (mode === 'bundles') { + return this.getWidgetsBundles().pipe( + map(bundles => search ? bundles.filter( + bundle => ( + bundle.title?.toLowerCase().includes(search.toLowerCase()) || + bundle.description?.toLowerCase().includes(search.toLowerCase()) + )) : bundles + ) + ); + } else { + return of([]); + } } - private fetchWidget(search: string, filter: widgetType[]): Observable> { + private fetchWidgets(search: string, filter: widgetType[], listMode: widgetsListMode): Observable> { return this.getWidgets().pipe( + map(widgets => (listMode && listMode !== 'all') ? + widgets.filter((widget) => listMode === 'actual' ? !widget.deprecated : widget.deprecated) : widgets), map(widgets => filter ? widgets.filter((widget) => filter.includes(widget.type)) : widgets), map(widgets => search ? widgets.filter( widget => ( @@ -198,4 +229,37 @@ export class DashboardWidgetSelectComponent implements OnInit { ) ); } + + private fetchAllWidgets(search: string, mode: selectWidgetMode): Observable> { + if (mode === 'allWidgets') { + const pageLink = new PageLink(1024, 0, search, { + property: 'name', + direction: Direction.ASC + }); + return this.getAllWidgets(pageLink); + } else { + return of([]); + } + } + + private getAllWidgets(pageLink: PageLink): Observable> { + this.loadingWidgetsSubject.next(true); + return this.widgetsService.getWidgetTypes(pageLink, false, true).pipe( + map(data => data.data.map(w => this.toWidgetInfo(w))), + tap(() => { + this.loadingWidgetsSubject.next(false); + }) + ); + } + + private toWidgetInfo(widgetTypeInfo: WidgetTypeInfo): WidgetInfo { + return { + typeFullFqn: fullWidgetTypeFqn(widgetTypeInfo), + type: widgetTypeInfo.widgetType, + title: widgetTypeInfo.name, + image: widgetTypeInfo.image, + description: widgetTypeInfo.description, + deprecated: widgetTypeInfo.deprecated + }; + } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html index db9af1ba06..f4cd14e6e5 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html @@ -18,12 +18,12 @@
-
-
- - -
+
+ +
+ + +
@@ -60,7 +72,9 @@ [stateController]="stateController" [dashboardTimewindow]="dashboard.configuration.timewindow" [widget]="widget" - [widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"> + [widgetConfig]="widgetFormGroup.get('widgetConfig').value.config" + [previewWidth]="widgetConfig.typeParameters.previewWidth" + [previewHeight]="widgetConfig.typeParameters.previewHeight">
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss index c31f1d5791..9c4e20a7a5 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss @@ -16,7 +16,7 @@ :host { .widget-preview-background { position: absolute; - top: 72px; + top: 68px; left: 0; right: 0; bottom: 0; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts index 2528ada4ae..e5a32a7a3e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -26,6 +26,7 @@ import { WidgetComponentService } from '@home/components/widget/widget-component import { WidgetConfigComponentData } from '../../models/widget-component.models'; import { isDefined, isDefinedAndNotNull, isString } from '@core/utils'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; @Component({ selector: 'tb-edit-widget', @@ -34,6 +35,9 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; }) export class EditWidgetComponent extends PageComponent implements OnInit, OnChanges { + @ViewChild('widgetConfigComponent') + widgetConfigComponent: WidgetConfigComponent; + @Input() dashboard: Dashboard; @@ -64,17 +68,12 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan previewMode = false; - hasBasicMode = false; - get widgetConfigMode(): WidgetConfigMode { - return this.hasBasicMode ? (this.widgetConfig?.config?.configMode || WidgetConfigMode.advanced) : WidgetConfigMode.advanced; + return this.widgetConfigComponent?.widgetConfigMode; } set widgetConfigMode(widgetConfigMode: WidgetConfigMode) { - if (this.hasBasicMode) { - this.widgetConfig.config.configMode = widgetConfigMode; - this.widgetFormGroup.markAsDirty(); - } + this.widgetConfigComponent.setWidgetConfigMode(widgetConfigMode); } private currentWidgetConfigChanged = false; @@ -169,9 +168,9 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan settingsDirective: widgetInfo.settingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective, + hasBasicMode: isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false, basicModeDirective: widgetInfo.basicModeDirective }; - this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false; this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); } } 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 eaf238d26e..d8b511f076 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 @@ -16,14 +16,14 @@ -->
-
+
- + { this.onGridsterParentResize(); }); - this.gridsterResize$.observe(this.gridster.el); + this.gridsterResize$.observe(this.gridster.el.parentElement); } onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number, persist?: boolean): void { 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 15df7e99e5..748723197f 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 @@ -21,7 +21,7 @@
- {{ headerTitle }} + {{ headerTitle }}
{{ headerSubtitle }} diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.scss b/ui-ngx/src/app/modules/home/components/details-panel.component.scss index 9002246841..451795a2d1 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.scss @@ -32,16 +32,14 @@ max-height: 120px; &.tb-details-title-header { min-width: 0; + padding: 0 16px 0 8px; } } .tb-details-title { width: inherit; margin: 20px 8px 0 0; - overflow: hidden; font-size: 1rem; font-weight: 400; - text-overflow: ellipsis; - white-space: nowrap; @media #{$mat-gt-sm} { font-size: 1.5rem; @@ -49,13 +47,16 @@ } .tb-details-subtitle { - width: inherit; margin: 10px 0; - overflow: hidden; font-size: 1rem; + opacity: .8; + } + + .tb-details-title-text, .tb-details-subtitle { + width: inherit; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - opacity: .8; } tb-dashboard { diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index 3b539cdba5..9d44404e8f 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -86,8 +86,7 @@ matTooltip="{{ actionDescriptor.name }}" matTooltipPosition="above" (click)="actionDescriptor.onAction($event)"> - - {{actionDescriptor.icon}} + {{actionDescriptor.icon}} + + + +
+
+ {{ 'widgets-bundle.export-widgets-bundle-widgets-prompt' | translate }} +
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/import-export/export-widgets-bundle-dialog.component.ts b/ui-ngx/src/app/modules/home/components/import-export/export-widgets-bundle-dialog.component.ts new file mode 100644 index 0000000000..570beaf7e8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/import-export/export-widgets-bundle-dialog.component.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2023 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, 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 { FormControl } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; + +export interface ExportWidgetsBundleDialogData { + widgetsBundle: WidgetsBundle; +} + +export interface ExportWidgetsBundleDialogResult { + exportWidgets: boolean; +} + +@Component({ + selector: 'tb-export-widgets-bundle-dialog', + templateUrl: './export-widgets-bundle-dialog.component.html', + providers: [], + styleUrls: [] +}) +export class ExportWidgetsBundleDialogComponent extends DialogComponent + implements OnInit { + + widgetsBundle: WidgetsBundle; + + exportWidgetsFormControl = new FormControl(false); + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ExportWidgetsBundleDialogData, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + this.widgetsBundle = data.widgetsBundle; + } + + ngOnInit(): void { + } + + cancel(): void { + this.dialogRef.close(null); + } + + export(): void { + this.dialogRef.close({ + exportWidgets: this.exportWidgetsFormControl.value + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts index fddfa80d91..4b41f00994 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts @@ -25,7 +25,8 @@ export interface ImportWidgetResult { export interface WidgetsBundleItem { widgetsBundle: WidgetsBundle; - widgetTypes: WidgetTypeDetails[]; + widgetTypes?: WidgetTypeDetails[]; + widgetTypeFqns?: string[]; } export interface CsvToJsonConfig { diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts index a6e7e7f89e..7ffcaae8e3 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts @@ -21,7 +21,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models'; -import { deepClone, isDefined, isObject, isString, isUndefined } from '@core/utils'; +import { deepClone, guid, isDefined, isObject, isString, isUndefined } from '@core/utils'; import { WINDOW } from '@core/services/window.service'; import { DOCUMENT } from '@angular/common'; import { @@ -34,8 +34,8 @@ import { } from '@shared/models/alias.models'; import { MatDialog } from '@angular/material/dialog'; import { ImportDialogComponent, ImportDialogData } from '@home/components/import-export/import-dialog.component'; -import { forkJoin, Observable, of } from 'rxjs'; -import { catchError, map, mergeMap, tap } from 'rxjs/operators'; +import { forkJoin, Observable, of, Subject } from 'rxjs'; +import { catchError, map, mergeMap, switchMap, tap } from 'rxjs/operators'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { EntityService } from '@core/http/entity.service'; import { Widget, WidgetSize, WidgetType, WidgetTypeDetails } from '@shared/models/widget.models'; @@ -58,7 +58,6 @@ import { import { EntityType } from '@shared/models/entity-type.models'; import { UtilsService } from '@core/services/utils.service'; import { WidgetService } from '@core/http/widget.service'; -import { NULL_UUID } from '@shared/models/id/has-uuid'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models'; import { RequestConfig } from '@core/http/http-utils'; @@ -75,6 +74,11 @@ import { EdgeService } from '@core/http/edge.service'; import { RuleNode } from '@shared/models/rule-node.models'; import { AssetProfileService } from '@core/http/asset-profile.service'; import { AssetProfile } from '@shared/models/asset.models'; +import { + ExportWidgetsBundleDialogComponent, + ExportWidgetsBundleDialogData, + ExportWidgetsBundleDialogResult +} from '@home/components/import-export/export-widgets-bundle-dialog.component'; // @dynamic @Injectable() @@ -180,6 +184,7 @@ export class ImportExportService { } else { let widget = widgetItem.widget; widget = this.dashboardUtils.validateAndUpdateWidget(widget); + widget.id = guid(); const aliasesInfo = this.prepareAliasesInfo(widgetItem.aliasesInfo); const filtersInfo: FiltersInfo = widgetItem.filtersInfo || { datasourceFilters: {} @@ -261,9 +266,6 @@ export class ImportExportService { public exportWidgetType(widgetTypeId: string) { this.widgetService.getWidgetTypeById(widgetTypeId).subscribe( (widgetTypeDetails) => { - if (isDefined(widgetTypeDetails.bundleAlias)) { - delete widgetTypeDetails.bundleAlias; - } let name = widgetTypeDetails.name; name = name.toLowerCase().replace(/\W/g, '_'); this.exportToPc(this.prepareExport(widgetTypeDetails), name); @@ -274,7 +276,28 @@ export class ImportExportService { ); } - public importWidgetType(bundleAlias: string): Observable { + public exportWidgetTypes(widgetTypeIds: string[]): Observable { + const widgetTypesObservables: Array> = []; + for (const id of widgetTypeIds) { + widgetTypesObservables.push(this.widgetService.getWidgetTypeById(id)); + } + return forkJoin(widgetTypesObservables).pipe( + map((widgetTypes) => + Object.fromEntries(widgetTypes.map(wt=> { + let name = wt.name; + name = name.toLowerCase().replace(/\W/g, '_') + `.${JSON_TYPE.extension}`; + const data = JSON.stringify(this.prepareExport(wt), null, 2); + return [name, data]; + }))), + mergeMap(widgetTypeFiles => this.exportJSZip(widgetTypeFiles, 'widget_types')), + catchError(e => { + this.handleExportError(e, 'widget-type.export-failed-error'); + throw e; + }) + ); + } + + public importWidgetType(): Observable { return this.openImportDialog('widget-type.import', 'widget-type.widget-type-file').pipe( mergeMap((widgetTypeDetails: WidgetTypeDetails) => { if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) { @@ -283,7 +306,6 @@ export class ImportExportService { type: 'error'})); throw new Error('Invalid widget type file'); } else { - widgetTypeDetails.bundleAlias = bundleAlias; return this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails); } }), @@ -296,27 +318,22 @@ export class ImportExportService { public exportWidgetsBundle(widgetsBundleId: string) { this.widgetService.getWidgetsBundle(widgetsBundleId).subscribe( (widgetsBundle) => { - const bundleAlias = widgetsBundle.alias; - const isSystem = widgetsBundle.tenantId.id === NULL_UUID; - this.widgetService.getBundleWidgetTypesDetails(bundleAlias, isSystem).subscribe( - (widgetTypesDetails) => { - widgetTypesDetails = widgetTypesDetails.sort((a, b) => a.createdTime - b.createdTime); - const widgetsBundleItem: WidgetsBundleItem = { - widgetsBundle: this.prepareExport(widgetsBundle), - widgetTypes: [] - }; - for (const widgetTypeDetails of widgetTypesDetails) { - if (isDefined(widgetTypeDetails.bundleAlias)) { - delete widgetTypeDetails.bundleAlias; + this.dialog.open(ExportWidgetsBundleDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + widgetsBundle + } + }).afterClosed().subscribe( + (result) => { + if (result) { + if (result.exportWidgets) { + this.exportWidgetsBundleWithWidgetTypes(widgetsBundle); + } else { + this.exportWidgetsBundleWithWidgetTypeFqns(widgetsBundle); } - widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetTypeDetails)); } - let name = widgetsBundle.title; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(widgetsBundleItem, name); - }, - (e) => { - this.handleExportError(e, 'widgets-bundle.export-failed-error'); } ); }, @@ -326,6 +343,43 @@ export class ImportExportService { ); } + private exportWidgetsBundleWithWidgetTypes(widgetsBundle: WidgetsBundle) { + this.widgetService.getBundleWidgetTypesDetails(widgetsBundle.id.id).subscribe( + (widgetTypesDetails) => { + const widgetsBundleItem: WidgetsBundleItem = { + widgetsBundle: this.prepareExport(widgetsBundle), + widgetTypes: [] + }; + for (const widgetTypeDetails of widgetTypesDetails) { + widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetTypeDetails)); + } + let name = widgetsBundle.title; + name = name.toLowerCase().replace(/\W/g, '_'); + this.exportToPc(widgetsBundleItem, name); + }, + (e) => { + this.handleExportError(e, 'widgets-bundle.export-failed-error'); + } + ); + } + + private exportWidgetsBundleWithWidgetTypeFqns(widgetsBundle: WidgetsBundle) { + this.widgetService.getBundleWidgetTypeFqns(widgetsBundle.id.id).subscribe( + (widgetTypeFqns) => { + const widgetsBundleItem: WidgetsBundleItem = { + widgetsBundle: this.prepareExport(widgetsBundle), + widgetTypeFqns + }; + let name = widgetsBundle.title; + name = name.toLowerCase().replace(/\W/g, '_'); + this.exportToPc(widgetsBundleItem, name); + }, + (e) => { + this.handleExportError(e, 'widgets-bundle.export-failed-error'); + } + ); + } + public importWidgetsBundle(): Observable { return this.openImportDialog('widgets-bundle.import', 'widgets-bundle.widgets-bundle-file').pipe( mergeMap((widgetsBundleItem: WidgetsBundleItem) => { @@ -338,16 +392,32 @@ export class ImportExportService { const widgetsBundle = widgetsBundleItem.widgetsBundle; return this.widgetService.saveWidgetsBundle(widgetsBundle).pipe( mergeMap((savedWidgetsBundle) => { - const bundleAlias = savedWidgetsBundle.alias; - const widgetTypesDetails = widgetsBundleItem.widgetTypes; - if (widgetTypesDetails.length) { - const saveWidgetTypesObservables: Array> = []; - for (const widgetTypeDetails of widgetTypesDetails) { - widgetTypeDetails.bundleAlias = bundleAlias; - saveWidgetTypesObservables.push(this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails)); + if (widgetsBundleItem.widgetTypes?.length || widgetsBundleItem.widgetTypeFqns?.length) { + let widgetTypesObservable: Observable>; + if (widgetsBundleItem.widgetTypes?.length) { + const widgetTypesDetails = widgetsBundleItem.widgetTypes; + const saveWidgetTypesObservables: Array> = []; + for (const widgetTypeDetails of widgetTypesDetails) { + saveWidgetTypesObservables.push(this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails)); + } + widgetTypesObservable = forkJoin(saveWidgetTypesObservables); + } else { + widgetTypesObservable = of([]); } - return forkJoin(saveWidgetTypesObservables).pipe( - map(() => savedWidgetsBundle) + return widgetTypesObservable.pipe( + switchMap((widgetTypes) => { + let widgetTypeFqns = widgetTypes.map(w => w.fqn); + if (widgetsBundleItem.widgetTypeFqns?.length) { + widgetTypeFqns = widgetTypeFqns.concat(widgetsBundleItem.widgetTypeFqns); + } + if (widgetTypeFqns.length) { + return this.widgetService.updateWidgetsBundleWidgetFqns(savedWidgetsBundle.id.id, widgetTypeFqns).pipe( + map((res) => savedWidgetsBundle) + ); + } else { + return of(savedWidgetsBundle); + } + }) ); } else { return of(savedWidgetsBundle); @@ -636,19 +706,28 @@ export class ImportExportService { this.downloadFile(content, filename, TEXT_TYPE); } - public exportJSZip(data: object, filename: string) { + public exportJSZip(data: object, filename: string): Observable { + const exportJsSubjectSubject = new Subject(); import('jszip').then((JSZip) => { - const jsZip = new JSZip.default(); - for (const keyName in data) { - if (data.hasOwnProperty(keyName)) { - const valueData = data[keyName]; - jsZip.file(keyName, valueData); + try { + const jsZip = new JSZip.default(); + for (const keyName in data) { + if (data.hasOwnProperty(keyName)) { + const valueData = data[keyName]; + jsZip.file(keyName, valueData); + } } + jsZip.generateAsync({type: 'blob'}).then(content => { + this.downloadFile(content, filename, ZIP_TYPE); + exportJsSubjectSubject.next(null); + }).catch(e => { + exportJsSubjectSubject.error(e); + }); + } catch (e) { + exportJsSubjectSubject.error(e); } - jsZip.generateAsync({type: 'blob'}).then(content => { - this.downloadFile(content, filename, ZIP_TYPE); - }); }); + return exportJsSubjectSubject.asObservable(); } private prepareRuleChain(ruleChain: RuleChain): RuleChain { @@ -744,9 +823,9 @@ export class ImportExportService { return false; } const widget = widgetItem.widget; - if (isUndefined(widget.isSystemType) || - isUndefined(widget.bundleAlias) || - isUndefined(widget.typeAlias) || + if ((isUndefined(widget.typeFullFqn) && (isUndefined((widget as any).isSystemType) || + isUndefined((widget as any).bundleAlias) || + isUndefined((widget as any).typeAlias))) || isUndefined(widget.type)) { return false; } @@ -765,16 +844,23 @@ export class ImportExportService { if (isUndefined(widgetsBundleItem.widgetsBundle)) { return false; } - if (isUndefined(widgetsBundleItem.widgetTypes)) { + if (isUndefined(widgetsBundleItem.widgetTypes) && isUndefined(widgetsBundleItem.widgetTypeFqns)) { return false; } const widgetsBundle = widgetsBundleItem.widgetsBundle; if (isUndefined(widgetsBundle.title)) { return false; } - const widgetTypesDetails = widgetsBundleItem.widgetTypes; - for (const widgetTypeDetails of widgetTypesDetails) { - if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) { + if (isDefined(widgetsBundleItem.widgetTypes)) { + const widgetTypesDetails = widgetsBundleItem.widgetTypes; + for (const widgetTypeDetails of widgetTypesDetails) { + if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) { + return false; + } + } + } + if (isDefined(widgetsBundleItem.widgetTypeFqns)) { + if (!Array.isArray(widgetsBundleItem.widgetTypeFqns)) { return false; } } @@ -982,6 +1068,7 @@ export class ImportExportService { } private prepareDashboardExport(dashboard: Dashboard): Dashboard { + dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard); dashboard = this.prepareExport(dashboard); delete dashboard.assignedCustomers; return dashboard; diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss index ad664e3156..1b796ccbda 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss @@ -16,7 +16,8 @@ :host { display: block; .clear-alarm-rule { - max-width: 100%; + min-width: 0; + margin-right: 8px; border: 2px groove rgba(0, 0, 0, .45); border-radius: 4px; padding: 8px; diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html index 87ee2a7fa1..89e1e1ddc4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html @@ -35,7 +35,7 @@ (click)="clear()"> close - + close - { id: guid(), consumerPerPartition: true, name: 'Main', - packProcessingTimeout: 2000, - partitions: 10, - pollInterval: 25, + packProcessingTimeout: 10000, + partitions: 1, + pollInterval: 2000, processingStrategy: { failurePercentage: 0, maxPauseBetweenRetries: 3, @@ -74,7 +74,56 @@ export class TenantProfileComponent extends EntityComponent { }, topic: 'tb_rule_engine.main', additionalInfo: { - description: '' + description: '', + customProperties: '' + } + }, + { + id: guid(), + name: 'HighPriority', + topic: 'tb_rule_engine.hp', + pollInterval: 2000, + partitions: 1, + consumerPerPartition: true, + packProcessingTimeout: 10000, + submitStrategy: { + type: 'BURST', + batchSize: 100 + }, + processingStrategy: { + type: 'RETRY_FAILED_AND_TIMED_OUT', + retries: 0, + failurePercentage: 0, + pauseBetweenRetries: 5, + maxPauseBetweenRetries: 5 + }, + additionalInfo: { + description: '', + customProperties: '' + } + }, + { + id: guid(), + name: 'SequentialByOriginator', + topic: 'tb_rule_engine.sq', + pollInterval: 2000, + partitions: 1, + consumerPerPartition: true, + packProcessingTimeout: 10000, + submitStrategy: { + type: 'SEQUENTIAL_BY_ORIGINATOR', + batchSize: 100 + }, + processingStrategy: { + type: 'RETRY_FAILED_AND_TIMED_OUT', + retries: 3, + failurePercentage: 0, + pauseBetweenRetries: 5, + maxPauseBetweenRetries: 5 + }, + additionalInfo: { + description: '', + customProperties: '' } } ]; @@ -118,9 +167,6 @@ export class TenantProfileComponent extends EntityComponent { if (this.entityForm) { if (this.isEditValue) { this.entityForm.enable({emitEvent: false}); - if (!this.isAdd) { - this.entityForm.get('isolatedTbRuleEngine').disable({emitEvent: false}); - } } else { this.entityForm.disable({emitEvent: false}); } diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html index 56b20d8aa8..960e81f8b4 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html @@ -203,6 +203,11 @@ + + queue.custom-properties + + queue.custom-properties-hint + queue.description diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts index b123cf2ad5..e4fbcd031b 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts @@ -117,7 +117,8 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestr }), topic: [''], additionalInfo: this.fb.group({ - description: [''] + description: [''], + customProperties: [''] }) }); this.valueChange$ = this.queueFormGroup.valueChanges.subscribe(() => { diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html index b052a1816c..77b87ced08 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html @@ -15,49 +15,47 @@ limitations under the License. --> -
-
-
-
+
+
+
{{ 'relation.type' | translate }}
+
{{ 'entity.entity-types' | translate }}
+
+
+
+
-
-
- - - - -
- +
+
+
+ relation.any-relation
-
- relation.any-relation +
+
-
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss index a2d4232f24..648076be4c 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss @@ -14,43 +14,15 @@ * limitations under the License. */ :host { - .tb-relation-filters { - max-width: calc(100vw - 48px); - margin-top: 2px; - overflow: hidden; - - .container{ - width: 100%; - } - - .map-label { - font-weight: 400; - font-size: 12px; - } - - .body { - max-height: 363px; - overflow: auto; - - .row { - padding-top: 5px; - - .input-block { - border: 1px solid #E0E0E0; - width: 100%; - border-radius: 6px; - padding: 24px; - align-items: center; - } - } - } + .flex-50 { + flex: 1 1 50%; + } - .any-filter{ - margin: 10px 0 20px; - } + .actions-header { + width: 40px + } - .add-button { - margin: 5px 0px 15px; - } + .entity-type-list { + display: flex; } } diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index c96b22a1cc..cf1186fa67 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -62,7 +62,7 @@   +
+ + + + +
+ + +
+
+ + + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ + + f() + + + + + + + + {{ (modelValue?.aggregationType || aggregationTypes.NONE) }} + ({{ 'datakey.latest-value' | translate }}) + + + ({{ 'datakey.delta' | translate }}:{{ (modelValue?.comparisonResultType === comparisonResultTypes.DELTA_PERCENT ? 'datakey.percent' : 'datakey.absolute') | translate }}) + ({{ 'datakey.delta-calculation-result-previous-value' | translate }}) + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.scss new file mode 100644 index 0000000000..ce12a6fde4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.scss @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2023 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-aggregated-data-key-row { + .mat-mdc-form-field.tb-inline-field.tb-aggregation-field { + .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { + padding-left: 8px; + padding-right: 0; + .mat-mdc-form-field-infix { + padding-top: 0; + padding-bottom: 6px; + .mdc-evolution-chip-set .mdc-evolution-chip { + margin: 0; + } + input.mat-mdc-chip-input { + height: 32px; + margin-left: 0; + } + } + } + .mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { + .tb-attribute-chip { + .tb-chip-labels { + background: transparent; + } + } + } + } + + .tb-position-field { + width: 132px; + min-width: 132px; + } + + .tb-aggregation-field { + flex: 1; + min-width: 120px; + } + + .tb-units-field, .tb-decimals-field, .tb-font-field, .tb-color-field, tb-arrow-field { + display: flex; + flex-direction: row; + place-content: center; + align-items: center; + } + + .tb-units-field { + width: 80px; + min-width: 80px; + } + + .tb-decimals-field { + width: 60px; + min-width: 60px; + } + + .tb-font-field, .tb-color-field, .tb-arrow-field { + width: 40px; + min-width: 40px; + display: none; + @media #{$mat-gt-xs} { + display: block; + } + } + + .tb-units-field, .tb-decimals-field { + display: none; + @media #{$mat-gt-sm} { + display: block; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts new file mode 100644 index 0000000000..3048179241 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts @@ -0,0 +1,249 @@ +/// +/// Copyright © 2016-2023 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, + EventEmitter, + forwardRef, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { + ComparisonResultType, + DataKey, + DataKeyConfigMode, + DatasourceType, JsonSettingsSchema, Widget, + widgetType +} from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { AggregationType } from '@shared/models/time/time.models'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { TranslateService } from '@ngx-translate/core'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { + DataKeyConfigDialogComponent, + DataKeyConfigDialogData +} from '@home/components/widget/config/data-key-config-dialog.component'; +import { deepClone, formatValue } from '@core/utils'; +import { + AggregatedValueCardKeyPosition, + aggregatedValueCardKeyPositionTranslations, + AggregatedValueCardKeySettings +} from '@home/components/widget/lib/cards/aggregated-value-card.models'; + +@Component({ + selector: 'tb-aggregated-data-key-row', + templateUrl: './aggregated-data-key-row.component.html', + styleUrls: ['./aggregated-data-key-row.component.scss', '../../data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AggregatedDataKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges { + + aggregatedValueCardKeyPositions: AggregatedValueCardKeyPosition[] = + Object.keys(AggregatedValueCardKeyPosition).map(value => AggregatedValueCardKeyPosition[value]); + + aggregatedValueCardKeyPositionTranslationMap = aggregatedValueCardKeyPositionTranslations; + + dataKeyTypes = DataKeyType; + + aggregationTypes = AggregationType; + + comparisonResultTypes = ComparisonResultType; + + @Input() + disabled: boolean; + + @Input() + datasourceType: DatasourceType; + + @Input() + keyName: string; + + @Output() + keyRemoved = new EventEmitter(); + + keyRowFormGroup: UntypedFormGroup; + + modelValue: DataKey; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + get callbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + get widget(): Widget { + return this.widgetConfigComponent.widget; + } + + get latestDataKeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.latestDataKeySettingsSchema; + } + + get latestDataKeySettingsDirective(): string { + return this.widgetConfigComponent.modelValue?.latestDataKeySettingsDirective; + } + + get isEntityDatasource(): boolean { + return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef, + public translate: TranslateService, + public truncate: TruncatePipe, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.keyRowFormGroup = this.fb.group({ + position: [null, []], + units: [null, []], + decimals: [null, []], + font: [null, []], + color: [null, []], + showArrow: [null, []] + }); + this.keyRowFormGroup.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 (['keyName'].includes(propName)) { + if (change.currentValue) { + this.modelValue.name = change.currentValue; + setTimeout(() => { + this.updateModel(); + }, 0); + } + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keyRowFormGroup.disable({emitEvent: false}); + } else { + this.keyRowFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKey): void { + this.modelValue = value || {} as DataKey; + const settings: AggregatedValueCardKeySettings = (this.modelValue.settings || {}); + this.keyRowFormGroup.patchValue( + { + position: settings.position || AggregatedValueCardKeyPosition.center, + units: value?.units, + decimals: value?.decimals, + font: settings.font, + color: settings.color, + showArrow: settings.showArrow + }, {emitEvent: false} + ); + this.cd.markForCheck(); + } + + dataKeyHasPostprocessing(): boolean { + return !!this.modelValue?.postFuncBody; + } + + editKey() { + this.dialog.open(DataKeyConfigDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataKey: deepClone(this.modelValue), + dataKeyConfigMode: DataKeyConfigMode.general, + dataKeySettingsSchema: this.latestDataKeySettingsSchema, + dataKeySettingsDirective: this.latestDataKeySettingsDirective, + dashboard: null, + aliasController: null, + widget: this.widget, + widgetType: widgetType.latest, + deviceId: null, + entityAliasId: null, + showPostProcessing: true, + callbacks: this.callbacks, + hideDataKeyName: true, + hideDataKeyLabel: true, + hideDataKeyColor: true + } + }).afterClosed().subscribe((updatedDataKey) => { + if (updatedDataKey) { + this.modelValue = updatedDataKey; + const settings: AggregatedValueCardKeySettings = (this.modelValue.settings || {}); + this.keyRowFormGroup.get('position').patchValue(settings.position, {emitEvent: false}); + this.keyRowFormGroup.get('font').patchValue(settings.font, {emitEvent: false}); + this.keyRowFormGroup.get('color').patchValue(settings.color, {emitEvent: false}); + this.keyRowFormGroup.get('showArrow').patchValue(settings.showArrow, {emitEvent: false}); + this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false}); + this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false}); + this.updateModel(); + } + }); + } + + private updateModel() { + const value = this.keyRowFormGroup.value; + this.modelValue.settings = this.modelValue.settings || {}; + this.modelValue.settings.position = value.position; + this.modelValue.settings.font = value.font; + this.modelValue.settings.color = value.color; + this.modelValue.settings.showArrow = value.showArrow; + this.modelValue.units = value.units; + this.modelValue.decimals = value.decimals; + this.propagateChange(this.modelValue); + } + + private _valuePreviewFn(): string { + const units: string = this.keyRowFormGroup.get('units').value; + const decimals: number = this.keyRowFormGroup.get('decimals').value; + return formatValue(22, decimals, units, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html new file mode 100644 index 0000000000..e507334aac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html @@ -0,0 +1,51 @@ + +
+
{{ 'widgets.aggregated-value-card.values' | translate }}
+
+
+
widgets.aggregated-value-card.position
+
widgets.aggregated-value-card.aggregation
+
widget-config.units-short
+
widget-config.decimals-short
+
widgets.aggregated-value-card.font
+
widgets.aggregated-value-card.color
+
widgets.aggregated-value-card.arrow
+
+
+
+
+ + +
+
+
+
+ +
+
+ + {{ 'widgets.aggregated-value-card.no-values' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.scss new file mode 100644 index 0000000000..6ce4e5b9b5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.scss @@ -0,0 +1,68 @@ +/** + * Copyright © 2016-2023 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-aggregated-data-keys-panel { + .tb-form-table-header-cell { + &.tb-position-header { + width: 132px; + min-width: 132px; + } + + &.tb-aggregation-header { + flex: 1; + min-width: 120px; + } + + &.tb-units-header { + width: 80px; + min-width: 80px; + } + + &.tb-decimals-header { + width: 60px; + min-width: 60px; + } + + &.tb-font-header, &.tb-color-header, &.tb-arrow-header { + width: 40px; + min-width: 40px; + display: none; + @media #{$mat-gt-xs} { + display: block; + } + } + + &.tb-actions-header { + width: 40px; + min-width: 40px; + } + + &.tb-units-header, &.tb-decimals-header { + display: none; + @media #{$mat-gt-sm} { + display: block; + } + } + } + .tb-form-table-body { + tb-aggregated-data-key-row { + overflow: hidden; + } + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts new file mode 100644 index 0000000000..d8b7f07f92 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts @@ -0,0 +1,173 @@ +/// +/// Copyright © 2016-2023 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, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { UtilsService } from '@core/services/utils.service'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { aggregatedValueCardDefaultKeySettings } from '@home/components/widget/lib/cards/aggregated-value-card.models'; + +@Component({ + selector: 'tb-aggregated-data-keys-panel', + templateUrl: './aggregated-data-keys-panel.component.html', + styleUrls: ['./aggregated-data-keys-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AggregatedDataKeysPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class AggregatedDataKeysPanelComponent implements ControlValueAccessor, OnInit, OnChanges { + + @Input() + disabled: boolean; + + @Input() + datasourceType: DatasourceType; + + @Input() + keyName: string; + + dataKeyType: DataKeyType; + + keysListFormGroup: UntypedFormGroup; + + get widgetType(): widgetType { + return this.widgetConfigComponent.widgetType; + } + + get callbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + get noKeys(): boolean { + const keys: DataKey[] = this.keysListFormGroup.get('keys').value; + return keys.length === 0; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef, + private utils: UtilsService, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.keysListFormGroup = this.fb.group({ + keys: [this.fb.array([]), []] + }); + this.keysListFormGroup.valueChanges.subscribe( + (val) => this.propagateChange(this.keysListFormGroup.get('keys').value) + ); + this.updateParams(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['datasourceType'].includes(propName)) { + this.updateParams(); + } + } + } + } + + private updateParams() { + if (this.datasourceType === DatasourceType.function) { + this.dataKeyType = DataKeyType.function; + } else { + this.dataKeyType = DataKeyType.timeseries; + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keysListFormGroup.disable({emitEvent: false}); + } else { + this.keysListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKey[] | undefined): void { + this.keysListFormGroup.setControl('keys', this.prepareKeysFormArray(value), {emitEvent: false}); + } + + keysFormArray(): UntypedFormArray { + return this.keysListFormGroup.get('keys') as UntypedFormArray; + } + + trackByKey(index: number, keyControl: AbstractControl): any { + return keyControl; + } + + removeKey(index: number) { + (this.keysListFormGroup.get('keys') as UntypedFormArray).removeAt(index); + } + + addKey() { + const dataKey = this.callbacks.generateDataKey(this.keyName, this.dataKeyType, null); + dataKey.decimals = 0; + dataKey.settings = {...aggregatedValueCardDefaultKeySettings}; + const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; + const keyControl = this.fb.control(dataKey, []); + keysArray.push(keyControl); + } + + private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray { + const keysControls: Array = []; + if (keys) { + keys.forEach((key) => { + keysControls.push(this.fb.control(key, [])); + }); + } + return this.fb.array(keysControls); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html new file mode 100644 index 0000000000..8f0b79cca6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html @@ -0,0 +1,149 @@ + + + + + + +
+
widget-config.appearance
+
+ + {{ 'widget-config.title' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.value-card.icon' | translate }} + +
+ + + + + + + + +
+
+
+ + {{ 'widgets.aggregated-value-card.subtitle' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.value-card.date' | translate }} + +
+ + + + + +
+
+
+ + {{ 'widgets.aggregated-value-card.chart' | translate }} + +
+ + + +
widget-config.decimals-suffix
+
+ + +
+
+
+ + +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts new file mode 100644 index 0000000000..f8e69c9d7a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts @@ -0,0 +1,305 @@ +/// +/// Copyright © 2016-2023 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, Injector } 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 { DataKey, Datasource, WidgetConfig, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; +import { isUndefined } from '@core/utils'; +import { + cssSizeToStrSize, + DateFormatProcessor, + DateFormatSettings, getDataKey, + resolveCssSize +} from '@shared/models/widget-settings.models'; +import { + aggregatedValueCardDefaultSettings, + AggregatedValueCardWidgetSettings, + createDefaultAggregatedValueLatestDataKeys +} from '@home/components/widget/lib/cards/aggregated-value-card.models'; +import { + AggregationType, + HistoryWindowType, + HOUR, + QuickTimeInterval, + TimewindowType +} from '@shared/models/time/time.models'; + +@Component({ + selector: 'tb-aggregated-value-card-basic-config', + templateUrl: './aggregated-value-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class AggregatedValueCardBasicConfigComponent extends BasicWidgetConfigComponent { + + public get datasource(): Datasource { + const datasources: Datasource[] = this.aggregatedValueCardWidgetConfigForm.get('datasources').value; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + public get keyName(): string { + const dataKey = getDataKey(this.aggregatedValueCardWidgetConfigForm.get('datasources').value); + if (dataKey) { + return dataKey.name; + } else { + return null; + } + } + + aggregatedValueCardWidgetConfigForm: UntypedFormGroup; + + datePreviewFn = this._datePreviewFn.bind(this); + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private cd: ChangeDetectorRef, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.aggregatedValueCardWidgetConfigForm; + } + + protected setupDefaults(configData: WidgetConfigComponentData) { + this.setupDefaultDatasource(configData, [ + { name: 'watermeter', label: 'Watermeter', type: DataKeyType.timeseries, color: 'rgba(0, 0, 0, 0.87)', units: 'm³', decimals: 0 } + ], + createDefaultAggregatedValueLatestDataKeys('watermeter', 'm³') + ); + configData.config.useDashboardTimewindow = false; + configData.config.displayTimewindow = true; + configData.config.timewindow = { + selectedTab: TimewindowType.HISTORY, + history: { + historyType: HistoryWindowType.INTERVAL, + quickInterval: QuickTimeInterval.CURRENT_MONTH_SO_FAR, + }, + aggregation: { + type: AggregationType.AVG, + interval: 12 * HOUR, + limit: 5000 + } + }; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: AggregatedValueCardWidgetSettings = {...aggregatedValueCardDefaultSettings, ...(configData.config.settings || {})}; + const dataKey = getDataKey(configData.config.datasources); + const keyName = dataKey?.name; + const iconSize = resolveCssSize(configData.config.iconSize); + this.aggregatedValueCardWidgetConfigForm = this.fb.group({ + timewindowConfig: [getTimewindowConfig(configData.config), []], + datasources: [configData.config.datasources, []], + + 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, []], + + showSubtitle: [settings.showSubtitle, []], + subtitle: [settings.subtitle, []], + subtitleFont: [settings.subtitleFont, []], + subtitleColor: [settings.subtitleColor, []], + + showDate: [settings.showDate, []], + dateFormat: [settings.dateFormat, []], + dateFont: [settings.dateFont, []], + dateColor: [settings.dateColor, []], + + showChart: [settings.showChart, []], + chartUnits: [dataKey?.units, []], + chartDecimals: [dataKey?.decimals, []], + chartColor: [dataKey?.color, []], + + values: [this.getValues(configData.config.datasources, keyName), []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); + this.widgetConfig.config.datasources = config.datasources; + + 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.showSubtitle = config.showSubtitle; + this.widgetConfig.config.settings.subtitle = config.subtitle; + this.widgetConfig.config.settings.subtitleFont = config.subtitleFont; + this.widgetConfig.config.settings.subtitleColor = config.subtitleColor; + + this.widgetConfig.config.settings.showDate = config.showDate; + this.widgetConfig.config.settings.dateFormat = config.dateFormat; + this.widgetConfig.config.settings.dateFont = config.dateFont; + this.widgetConfig.config.settings.dateColor = config.dateColor; + + this.widgetConfig.config.settings.showChart = config.showChart; + + const dataKey = getDataKey(this.widgetConfig.config.datasources); + if (dataKey) { + dataKey.units = config.chartUnits; + dataKey.decimals = config.chartDecimals; + dataKey.color = config.chartColor; + } + + this.setValues(config.values, this.widgetConfig.config.datasources); + + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showIcon', 'showSubtitle', 'showDate', 'showChart']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.aggregatedValueCardWidgetConfigForm.get('showTitle').value; + const showIcon: boolean = this.aggregatedValueCardWidgetConfigForm.get('showIcon').value; + const showSubtitle: boolean = this.aggregatedValueCardWidgetConfigForm.get('showSubtitle').value; + const showDate: boolean = this.aggregatedValueCardWidgetConfigForm.get('showDate').value; + const showChart: boolean = this.aggregatedValueCardWidgetConfigForm.get('showChart').value; + + if (showTitle) { + this.aggregatedValueCardWidgetConfigForm.get('title').enable(); + this.aggregatedValueCardWidgetConfigForm.get('titleFont').enable(); + this.aggregatedValueCardWidgetConfigForm.get('titleColor').enable(); + this.aggregatedValueCardWidgetConfigForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.aggregatedValueCardWidgetConfigForm.get('iconSize').enable(); + this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').enable(); + this.aggregatedValueCardWidgetConfigForm.get('icon').enable(); + this.aggregatedValueCardWidgetConfigForm.get('iconColor').enable(); + } else { + this.aggregatedValueCardWidgetConfigForm.get('iconSize').disable(); + this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').disable(); + this.aggregatedValueCardWidgetConfigForm.get('icon').disable(); + this.aggregatedValueCardWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.aggregatedValueCardWidgetConfigForm.get('title').disable(); + this.aggregatedValueCardWidgetConfigForm.get('titleFont').disable(); + this.aggregatedValueCardWidgetConfigForm.get('titleColor').disable(); + this.aggregatedValueCardWidgetConfigForm.get('showIcon').disable({emitEvent: false}); + this.aggregatedValueCardWidgetConfigForm.get('iconSize').disable(); + this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').disable(); + this.aggregatedValueCardWidgetConfigForm.get('icon').disable(); + this.aggregatedValueCardWidgetConfigForm.get('iconColor').disable(); + } + + if (showSubtitle) { + this.aggregatedValueCardWidgetConfigForm.get('subtitle').enable(); + this.aggregatedValueCardWidgetConfigForm.get('subtitleFont').enable(); + this.aggregatedValueCardWidgetConfigForm.get('subtitleColor').enable(); + } else { + this.aggregatedValueCardWidgetConfigForm.get('subtitle').disable(); + this.aggregatedValueCardWidgetConfigForm.get('subtitleFont').disable(); + this.aggregatedValueCardWidgetConfigForm.get('subtitleColor').disable(); + } + + if (showDate) { + this.aggregatedValueCardWidgetConfigForm.get('dateFormat').enable(); + this.aggregatedValueCardWidgetConfigForm.get('dateFont').enable(); + this.aggregatedValueCardWidgetConfigForm.get('dateColor').enable(); + } else { + this.aggregatedValueCardWidgetConfigForm.get('dateFormat').disable(); + this.aggregatedValueCardWidgetConfigForm.get('dateFont').disable(); + this.aggregatedValueCardWidgetConfigForm.get('dateColor').disable(); + } + + if (showChart) { + this.aggregatedValueCardWidgetConfigForm.get('chartUnits').enable(); + this.aggregatedValueCardWidgetConfigForm.get('chartDecimals').enable(); + this.aggregatedValueCardWidgetConfigForm.get('chartColor').enable(); + } else { + this.aggregatedValueCardWidgetConfigForm.get('chartUnits').disable(); + this.aggregatedValueCardWidgetConfigForm.get('chartDecimals').disable(); + this.aggregatedValueCardWidgetConfigForm.get('chartColor').disable(); + } + } + + private getValues(datasources: Datasource[], keyName: string): DataKey[] { + if (datasources && datasources.length) { + return (datasources[0].latestDataKeys || []).filter(k => k.name === keyName); + } + return []; + } + + private setValues(values: DataKey[], datasources?: Datasource[]) { + if (datasources && datasources.length) { + datasources[0].latestDataKeys = values; + } + } + + 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'); + } + + private _datePreviewFn(): string { + const dateFormat: DateFormatSettings = this.aggregatedValueCardWidgetConfigForm.get('dateFormat').value; + const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); + processor.update(Date.now()); + return processor.formatted; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html index c5501ae830..ae0ca13dbe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html @@ -39,13 +39,24 @@
widget-config.card-appearance
-
+
{{ 'widget-config.card-title' | translate }} - - - +
+ + + + + + + +
@@ -53,15 +64,17 @@
-
+
widget-config.show-card-buttons
{{ 'action.search' | translate }} @@ -72,12 +85,14 @@
{{ 'widget-config.text-color' | translate }}
{{ 'widget-config.background-color' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts index 2061832918..228879e2ef 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts @@ -29,7 +29,10 @@ import { import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { isUndefined } from '@core/utils'; -import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; @Component({ selector: 'tb-entities-table-basic-config', @@ -80,6 +83,8 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen columns: [this.getColumns(configData.config.datasources), []], showTitle: [configData.config.showTitle, []], title: [configData.config.settings?.entitiesTitle, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], showTitleIcon: [configData.config.showTitleIcon, []], titleIcon: [configData.config.titleIcon, []], iconColor: [configData.config.iconColor, []], @@ -91,15 +96,15 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen } protected prepareOutputConfig(config: any): WidgetConfigComponentData { - this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; - this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow; - this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow; + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); this.widgetConfig.config.datasources = config.datasources; this.setColumns(config.columns, this.widgetConfig.config.datasources); this.widgetConfig.config.actions = config.actions; this.widgetConfig.config.showTitle = config.showTitle; this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; this.widgetConfig.config.settings.entitiesTitle = config.title; + this.widgetConfig.config.titleFont = config.titleFont; + this.widgetConfig.config.titleColor = config.titleColor; this.widgetConfig.config.showTitleIcon = config.showTitleIcon; this.widgetConfig.config.titleIcon = config.titleIcon; this.widgetConfig.config.iconColor = config.iconColor; @@ -118,6 +123,8 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen const showTitleIcon: boolean = this.entitiesTableWidgetConfigForm.get('showTitleIcon').value; if (showTitle) { this.entitiesTableWidgetConfigForm.get('title').enable(); + this.entitiesTableWidgetConfigForm.get('titleFont').enable(); + this.entitiesTableWidgetConfigForm.get('titleColor').enable(); this.entitiesTableWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); if (showTitleIcon) { this.entitiesTableWidgetConfigForm.get('titleIcon').enable(); @@ -128,11 +135,15 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen } } else { this.entitiesTableWidgetConfigForm.get('title').disable(); + this.entitiesTableWidgetConfigForm.get('titleFont').disable(); + this.entitiesTableWidgetConfigForm.get('titleColor').disable(); this.entitiesTableWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); this.entitiesTableWidgetConfigForm.get('titleIcon').disable(); this.entitiesTableWidgetConfigForm.get('iconColor').disable(); } this.entitiesTableWidgetConfigForm.get('title').updateValueAndValidity({emitEvent}); + this.entitiesTableWidgetConfigForm.get('titleFont').updateValueAndValidity({emitEvent}); + this.entitiesTableWidgetConfigForm.get('titleColor').updateValueAndValidity({emitEvent}); this.entitiesTableWidgetConfigForm.get('showTitleIcon').updateValueAndValidity({emitEvent: false}); this.entitiesTableWidgetConfigForm.get('titleIcon').updateValueAndValidity({emitEvent}); this.entitiesTableWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent}); diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html index 142f32cd4e..92bd2e44ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html @@ -61,7 +61,7 @@
-
+
widget-config.show-card-buttons
{{ 'fullscreen.fullscreen' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts index 26d7b5ba61..9b2a6a764b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts @@ -21,14 +21,18 @@ 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 { - Datasource, datasourcesHasAggregation, - datasourcesHasOnlyComparisonAggregation, WidgetConfig, + datasourcesHasOnlyComparisonAggregation, + WidgetConfig, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; import { isUndefined } from '@core/utils'; +import { getLabel, setLabel } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-simple-card-basic-config', @@ -67,7 +71,7 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { this.simpleCardWidgetConfigForm = this.fb.group({ timewindowConfig: [getTimewindowConfig(configData.config), []], datasources: [configData.config.datasources, []], - label: [this.getDataKeyLabel(configData.config.datasources), []], + label: [getLabel(configData.config.datasources), []], labelPosition: [configData.config.settings?.labelPosition, []], units: [configData.config.units, []], decimals: [configData.config.decimals, []], @@ -79,11 +83,9 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { } protected prepareOutputConfig(config: any): WidgetConfigComponentData { - this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; - this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow; - this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow; + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); this.widgetConfig.config.datasources = config.datasources; - this.setDataKeyLabel(config.label, this.widgetConfig.config.datasources); + setLabel(config.label, this.widgetConfig.config.datasources); this.widgetConfig.config.actions = config.actions; this.widgetConfig.config.units = config.units; this.widgetConfig.config.decimals = config.decimals; @@ -95,25 +97,6 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { return this.widgetConfig; } - private getDataKeyLabel(datasources?: Datasource[]): string { - if (datasources && datasources.length) { - const dataKeys = datasources[0].dataKeys; - if (dataKeys && dataKeys.length) { - return dataKeys[0].label; - } - } - return ''; - } - - private setDataKeyLabel(label: string, datasources?: Datasource[]) { - if (datasources && datasources.length) { - const dataKeys = datasources[0].dataKeys; - if (dataKeys && dataKeys.length) { - dataKeys[0].label = label; - } - } - } - private getCardButtons(config: WidgetConfig): string[] { const buttons: string[] = []; if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html index bab6437485..15c903736a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html @@ -39,13 +39,24 @@
widget-config.card-appearance
-
+
{{ 'widget-config.card-title' | translate }} - - - +
+ + + + + + + +
@@ -53,15 +64,17 @@
-
+
widget-config.show-card-buttons
{{ 'action.search' | translate }} @@ -72,12 +85,14 @@
{{ 'widget-config.text-color' | translate }}
{{ 'widget-config.background-color' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts index ac1b12167c..dc2a41b816 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts @@ -24,7 +24,10 @@ import { DataKey, Datasource, WidgetConfig } from '@shared/models/widget.models' import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { deepClone, isUndefined } from '@core/utils'; -import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; @Component({ selector: 'tb-timeseries-table-basic-config', @@ -66,6 +69,8 @@ export class TimeseriesTableBasicConfigComponent extends BasicWidgetConfigCompon columns: [this.getColumns(configData.config.datasources), []], showTitle: [configData.config.showTitle, []], title: [configData.config.title, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], showTitleIcon: [configData.config.showTitleIcon, []], titleIcon: [configData.config.titleIcon, []], iconColor: [configData.config.iconColor, []], @@ -77,14 +82,14 @@ export class TimeseriesTableBasicConfigComponent extends BasicWidgetConfigCompon } protected prepareOutputConfig(config: any): WidgetConfigComponentData { - this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; - this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow; - this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow; + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); this.widgetConfig.config.datasources = config.datasources; this.setColumns(config.columns, this.widgetConfig.config.datasources); this.widgetConfig.config.actions = config.actions; 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.showTitleIcon; this.widgetConfig.config.titleIcon = config.titleIcon; this.widgetConfig.config.iconColor = config.iconColor; @@ -104,6 +109,8 @@ export class TimeseriesTableBasicConfigComponent extends BasicWidgetConfigCompon const showTitleIcon: boolean = this.timeseriesTableWidgetConfigForm.get('showTitleIcon').value; if (showTitle) { this.timeseriesTableWidgetConfigForm.get('title').enable(); + this.timeseriesTableWidgetConfigForm.get('titleFont').enable(); + this.timeseriesTableWidgetConfigForm.get('titleColor').enable(); this.timeseriesTableWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); if (showTitleIcon) { this.timeseriesTableWidgetConfigForm.get('titleIcon').enable(); @@ -114,11 +121,15 @@ export class TimeseriesTableBasicConfigComponent extends BasicWidgetConfigCompon } } else { this.timeseriesTableWidgetConfigForm.get('title').disable(); + this.timeseriesTableWidgetConfigForm.get('titleFont').disable(); + this.timeseriesTableWidgetConfigForm.get('titleColor').disable(); this.timeseriesTableWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); this.timeseriesTableWidgetConfigForm.get('titleIcon').disable(); this.timeseriesTableWidgetConfigForm.get('iconColor').disable(); } this.timeseriesTableWidgetConfigForm.get('title').updateValueAndValidity({emitEvent}); + this.timeseriesTableWidgetConfigForm.get('titleFont').updateValueAndValidity({emitEvent}); + this.timeseriesTableWidgetConfigForm.get('titleColor').updateValueAndValidity({emitEvent}); this.timeseriesTableWidgetConfigForm.get('showTitleIcon').updateValueAndValidity({emitEvent: false}); this.timeseriesTableWidgetConfigForm.get('titleIcon').updateValueAndValidity({emitEvent}); this.timeseriesTableWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent}); diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html new file mode 100644 index 0000000000..a036ab2b1c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html @@ -0,0 +1,125 @@ + + + + + + +
+
widget-config.appearance
+ + + {{ valueCardLayoutTranslationMap.get(layout) | translate }} + + +
+ + {{ 'widgets.value-card.label' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.value-card.icon' | translate }} + +
+ + + + + + + + +
+
+
+
widgets.value-card.value
+
+ + + +
widget-config.decimals-suffix
+
+ + + + +
+
+
+ + {{ 'widgets.value-card.date' | translate }} + +
+ + + + + +
+
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts new file mode 100644 index 0000000000..dacb2dfa11 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts @@ -0,0 +1,287 @@ +/// +/// Copyright © 2016-2023 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, Injector } 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 { + datasourcesHasAggregation, + datasourcesHasOnlyComparisonAggregation, + WidgetConfig, +} from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; +import { formatValue, isDefinedAndNotNull, isUndefined } from '@core/utils'; +import { + DateFormatProcessor, + DateFormatSettings, + getLabel, + setLabel +} from '@shared/models/widget-settings.models'; +import { + valueCardDefaultSettings, + ValueCardLayout, + valueCardLayoutImages, + valueCardLayouts, + valueCardLayoutTranslations, + ValueCardWidgetSettings +} from '@home/components/widget/lib/cards/value-card-widget.models'; + +@Component({ + selector: 'tb-value-card-basic-config', + templateUrl: './value-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent { + + public get displayTimewindowConfig(): boolean { + const datasources = this.valueCardWidgetConfigForm.get('datasources').value; + return datasourcesHasAggregation(datasources); + } + + public onlyHistoryTimewindow(): boolean { + const datasources = this.valueCardWidgetConfigForm.get('datasources').value; + return datasourcesHasOnlyComparisonAggregation(datasources); + } + + valueCardLayouts: ValueCardLayout[] = []; + + valueCardLayoutTranslationMap = valueCardLayoutTranslations; + valueCardLayoutImageMap = valueCardLayoutImages; + + horizontal = false; + + valueCardWidgetConfigForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + datePreviewFn = this._datePreviewFn.bind(this); + + get dateEnabled(): boolean { + const layout: ValueCardLayout = this.valueCardWidgetConfigForm.get('layout').value; + return ![ValueCardLayout.vertical, ValueCardLayout.simplified].includes(layout); + } + + get iconEnabled(): boolean { + const layout: ValueCardLayout = this.valueCardWidgetConfigForm.get('layout').value; + return layout !== ValueCardLayout.simplified; + } + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private cd: ChangeDetectorRef, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.valueCardWidgetConfigForm; + } + + protected setupConfig(widgetConfig: WidgetConfigComponentData) { + const params = widgetConfig.typeParameters as any; + this.horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false; + this.valueCardLayouts = valueCardLayouts(this.horizontal); + super.setupConfig(widgetConfig); + } + + protected setupDefaults(configData: WidgetConfigComponentData) { + this.setupDefaultDatasource(configData, [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries }]); + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: ValueCardWidgetSettings = {...valueCardDefaultSettings(this.horizontal), ...(configData.config.settings || {})}; + this.valueCardWidgetConfigForm = this.fb.group({ + timewindowConfig: [getTimewindowConfig(configData.config), []], + datasources: [configData.config.datasources, []], + layout: [settings.layout, []], + + showLabel: [settings.showLabel, []], + label: [getLabel(configData.config.datasources), []], + labelFont: [settings.labelFont, []], + labelColor: [settings.labelColor, []], + + showIcon: [settings.showIcon, []], + iconSize: [settings.iconSize, [Validators.min(0)]], + iconSizeUnit: [settings.iconSizeUnit, []], + icon: [settings.icon, []], + iconColor: [settings.iconColor, []], + + units: [configData.config.units, []], + decimals: [configData.config.decimals, []], + valueFont: [settings.valueFont, []], + valueColor: [settings.valueColor, []], + + showDate: [settings.showDate, []], + dateFormat: [settings.dateFormat, []], + dateFont: [settings.dateFont, []], + dateColor: [settings.dateColor, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); + this.widgetConfig.config.datasources = config.datasources; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.layout = config.layout; + + this.widgetConfig.config.settings.showLabel = config.showLabel; + setLabel(config.label, this.widgetConfig.config.datasources); + this.widgetConfig.config.settings.labelFont = config.labelFont; + this.widgetConfig.config.settings.labelColor = config.labelColor; + + this.widgetConfig.config.settings.showIcon = config.showIcon; + this.widgetConfig.config.settings.iconSize = config.iconSize; + this.widgetConfig.config.settings.iconSizeUnit = config.iconSizeUnit; + this.widgetConfig.config.settings.icon = config.icon; + this.widgetConfig.config.settings.iconColor = config.iconColor; + + this.widgetConfig.config.units = config.units; + this.widgetConfig.config.decimals = config.decimals; + this.widgetConfig.config.settings.valueFont = config.valueFont; + this.widgetConfig.config.settings.valueColor = config.valueColor; + + this.widgetConfig.config.settings.showDate = config.showDate; + this.widgetConfig.config.settings.dateFormat = config.dateFormat; + this.widgetConfig.config.settings.dateFont = config.dateFont; + this.widgetConfig.config.settings.dateColor = config.dateColor; + + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['layout', 'showLabel', 'showIcon', 'showDate']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const layout: ValueCardLayout = this.valueCardWidgetConfigForm.get('layout').value; + const showLabel: boolean = this.valueCardWidgetConfigForm.get('showLabel').value; + const showIcon: boolean = this.valueCardWidgetConfigForm.get('showIcon').value; + const showDate: boolean = this.valueCardWidgetConfigForm.get('showDate').value; + + const dateEnabled = ![ValueCardLayout.vertical, ValueCardLayout.simplified].includes(layout); + const iconEnabled = layout !== ValueCardLayout.simplified; + + if (showLabel) { + this.valueCardWidgetConfigForm.get('label').enable(); + this.valueCardWidgetConfigForm.get('labelFont').enable(); + this.valueCardWidgetConfigForm.get('labelColor').enable(); + } else { + this.valueCardWidgetConfigForm.get('label').disable(); + this.valueCardWidgetConfigForm.get('labelFont').disable(); + this.valueCardWidgetConfigForm.get('labelColor').disable(); + } + + if (iconEnabled) { + this.valueCardWidgetConfigForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.valueCardWidgetConfigForm.get('iconSize').enable(); + this.valueCardWidgetConfigForm.get('iconSizeUnit').enable(); + this.valueCardWidgetConfigForm.get('icon').enable(); + this.valueCardWidgetConfigForm.get('iconColor').enable(); + } else { + this.valueCardWidgetConfigForm.get('iconSize').disable(); + this.valueCardWidgetConfigForm.get('iconSizeUnit').disable(); + this.valueCardWidgetConfigForm.get('icon').disable(); + this.valueCardWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.valueCardWidgetConfigForm.get('showIcon').disable({emitEvent: false}); + this.valueCardWidgetConfigForm.get('iconSize').disable(); + this.valueCardWidgetConfigForm.get('iconSizeUnit').disable(); + this.valueCardWidgetConfigForm.get('icon').disable(); + this.valueCardWidgetConfigForm.get('iconColor').disable(); + } + + if (dateEnabled) { + this.valueCardWidgetConfigForm.get('showDate').enable({emitEvent: false}); + if (showDate) { + this.valueCardWidgetConfigForm.get('dateFormat').enable(); + this.valueCardWidgetConfigForm.get('dateFont').enable(); + this.valueCardWidgetConfigForm.get('dateColor').enable(); + } else { + this.valueCardWidgetConfigForm.get('dateFormat').disable(); + this.valueCardWidgetConfigForm.get('dateFont').disable(); + this.valueCardWidgetConfigForm.get('dateColor').disable(); + } + } else { + this.valueCardWidgetConfigForm.get('showDate').disable({emitEvent: false}); + this.valueCardWidgetConfigForm.get('dateFormat').disable(); + this.valueCardWidgetConfigForm.get('dateFont').disable(); + this.valueCardWidgetConfigForm.get('dateColor').disable(); + } + this.valueCardWidgetConfigForm.get('showIcon').updateValueAndValidity({emitEvent: false}); + this.valueCardWidgetConfigForm.get('showDate').updateValueAndValidity({emitEvent: false}); + this.valueCardWidgetConfigForm.get('label').updateValueAndValidity({emitEvent}); + this.valueCardWidgetConfigForm.get('labelFont').updateValueAndValidity({emitEvent}); + this.valueCardWidgetConfigForm.get('labelColor').updateValueAndValidity({emitEvent}); + this.valueCardWidgetConfigForm.get('iconSize').updateValueAndValidity({emitEvent}); + this.valueCardWidgetConfigForm.get('iconSizeUnit').updateValueAndValidity({emitEvent}); + this.valueCardWidgetConfigForm.get('icon').updateValueAndValidity({emitEvent}); + this.valueCardWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent}); + this.valueCardWidgetConfigForm.get('dateFormat').updateValueAndValidity({emitEvent}); + this.valueCardWidgetConfigForm.get('dateFont').updateValueAndValidity({emitEvent}); + this.valueCardWidgetConfigForm.get('dateColor').updateValueAndValidity({emitEvent}); + } + + 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'); + } + + private _valuePreviewFn(): string { + const units: string = this.valueCardWidgetConfigForm.get('units').value; + const decimals: number = this.valueCardWidgetConfigForm.get('decimals').value; + return formatValue(22, decimals, units, true); + } + + private _datePreviewFn(): string { + const dateFormat: DateFormatSettings = this.valueCardWidgetConfigForm.get('dateFormat').value; + const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); + processor.update(Date.now()); + return processor.formatted; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html index 1439f74931..6bd31f41eb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html @@ -39,13 +39,24 @@
widget-config.card-appearance
-
+
{{ 'widget-config.card-title' | translate }} - - - +
+ + + + + + + +
@@ -53,15 +64,17 @@
-
+
widget-config.show-card-buttons
{{ 'fullscreen.fullscreen' | translate }} @@ -70,12 +83,14 @@
{{ 'widget-config.text-color' | translate }}
{{ 'widget-config.background-color' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts index 9d3dc4f1dd..050d362868 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts @@ -24,7 +24,10 @@ import { DataKey, Datasource, WidgetConfig } from '@shared/models/widget.models' import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { isUndefined } from '@core/utils'; -import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; @Component({ selector: 'tb-flot-basic-config', @@ -66,6 +69,8 @@ export class FlotBasicConfigComponent extends BasicWidgetConfigComponent { series: [this.getSeries(configData.config.datasources), []], showTitle: [configData.config.showTitle, []], title: [configData.config.title, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], showTitleIcon: [configData.config.showTitleIcon, []], titleIcon: [configData.config.titleIcon, []], iconColor: [configData.config.iconColor, []], @@ -81,14 +86,14 @@ export class FlotBasicConfigComponent extends BasicWidgetConfigComponent { } protected prepareOutputConfig(config: any): WidgetConfigComponentData { - this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; - this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow; - this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow; + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); this.widgetConfig.config.datasources = config.datasources; this.setSeries(config.series, this.widgetConfig.config.datasources); this.widgetConfig.config.actions = config.actions; 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.showTitleIcon; this.widgetConfig.config.titleIcon = config.titleIcon; this.widgetConfig.config.iconColor = config.iconColor; @@ -114,6 +119,8 @@ export class FlotBasicConfigComponent extends BasicWidgetConfigComponent { const showLegend: boolean = this.flotWidgetConfigForm.get('showLegend').value; if (showTitle) { this.flotWidgetConfigForm.get('title').enable(); + this.flotWidgetConfigForm.get('titleFont').enable(); + this.flotWidgetConfigForm.get('titleColor').enable(); this.flotWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); if (showTitleIcon) { this.flotWidgetConfigForm.get('titleIcon').enable(); @@ -124,6 +131,8 @@ export class FlotBasicConfigComponent extends BasicWidgetConfigComponent { } } else { this.flotWidgetConfigForm.get('title').disable(); + this.flotWidgetConfigForm.get('titleFont').disable(); + this.flotWidgetConfigForm.get('titleColor').disable(); this.flotWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); this.flotWidgetConfigForm.get('titleIcon').disable(); this.flotWidgetConfigForm.get('iconColor').disable(); @@ -134,6 +143,8 @@ export class FlotBasicConfigComponent extends BasicWidgetConfigComponent { this.flotWidgetConfigForm.get('legendConfig').disable(); } this.flotWidgetConfigForm.get('title').updateValueAndValidity({emitEvent}); + this.flotWidgetConfigForm.get('titleFont').updateValueAndValidity({emitEvent}); + this.flotWidgetConfigForm.get('titleColor').updateValueAndValidity({emitEvent}); this.flotWidgetConfigForm.get('showTitleIcon').updateValueAndValidity({emitEvent: false}); this.flotWidgetConfigForm.get('titleIcon').updateValueAndValidity({emitEvent}); this.flotWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent}); diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html index b603f98a4d..63f936195a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html @@ -22,13 +22,13 @@ {{ 'datakey.latest' | translate }} - +
-
+
timeline
-
+
@@ -139,7 +139,7 @@ - +
@@ -158,7 +158,8 @@
-
+
+
datakey.delta-calculation
+
+ + + + + {{ 'datakey.enable-delta-calculation' | translate }} + + {{ 'datakey.enable-delta-calculation-hint' | translate }} + + + + +
+
{{ 'widgets.chart.time-for-comparison' | translate }}
+ + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} + + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} + + + +
+
+
{{ 'widgets.chart.custom-interval-value' | translate }}
+ + + +
+
+
{{ 'datakey.delta-calculation-result' | translate }}
+ + + + {{ comparisonResultTypeTranslations.get(comparisonResultTypes[comparisonResultType]) | translate }} + + + +
+
+
+
+
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 f37451d32d..10fe8a93c8 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 @@ -126,6 +126,10 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @Input() showPostProcessing = true; + @Input() + @coerceBoolean() + hideDataKeyName = false; + @Input() @coerceBoolean() hideDataKeyLabel = false; @@ -144,8 +148,8 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @ViewChild('keyInput') keyInput: ElementRef; - @ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent; - @ViewChild('postFuncBodyEdit') postFuncBodyEdit: JsFuncComponent; + @ViewChild('funcBodyEdit', {static: false}) funcBodyEdit: JsFuncComponent; + @ViewChild('postFuncBodyEdit', {static: false}) postFuncBodyEdit: JsFuncComponent; hasAdvanced = false; @@ -438,10 +442,11 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con } public validateOnSubmit() { - if (this.modelValue.type === DataKeyType.function) { + if (this.modelValue.type === DataKeyType.function && this.funcBodyEdit) { this.funcBodyEdit.validateOnSubmit(); } else if ((this.modelValue.type === DataKeyType.timeseries || - this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value) { + this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value && + this.postFuncBodyEdit) { this.postFuncBodyEdit.validateOnSubmit(); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html index 65daa6552c..3226ffed5e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html @@ -47,7 +47,7 @@ drag_indicator
-
+
{{key.label}}
:
-
+
-
+
-
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.scss new file mode 100644 index 0000000000..d3c2dfb4fb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.scss @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2023 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-timewindow-style-panel { + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-gt-xs} { + min-width: 320px; + } + .tb-timewindow-style-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-form-row { + .fixed-title-width { + min-width: 120px; + } + &.timewindow-preview { + align-items: flex-start; + tb-timewindow { + font-size: 14px; + opacity: .85; + } + } + } + .tb-timewindow-style-panel-buttons { + height: 60px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts new file mode 100644 index 0000000000..85e7e5a4be --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts @@ -0,0 +1,109 @@ +/// +/// Copyright © 2016-2023 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 { PageComponent } from '@shared/components/page.component'; +import { defaultTimewindowStyle, TimewindowStyle } from '@shared/models/widget-settings.models'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Timewindow } from '@shared/models/time/time.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-timewindow-style-panel', + templateUrl: './timewindow-style-panel.component.html', + providers: [], + styleUrls: ['./timewindow-style-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class TimewindowStylePanelComponent extends PageComponent implements OnInit { + + @Input() + timewindowStyle: TimewindowStyle; + + @Input() + previewValue: Timewindow; + + @Input() + popover: TbPopoverComponent; + + @Output() + timewindowStyleApplied = new EventEmitter(); + + timewindowStyleFormGroup: UntypedFormGroup; + + previewTimewindowStyle: TimewindowStyle; + + constructor(private fb: UntypedFormBuilder, + protected store: Store) { + super(store); + } + + ngOnInit(): void { + const computedTimewindowStyle = {...defaultTimewindowStyle, ...(this.timewindowStyle || {})}; + this.timewindowStyleFormGroup = this.fb.group( + { + showIcon: [computedTimewindowStyle.showIcon, []], + iconSize: [computedTimewindowStyle.iconSize, []], + icon: [computedTimewindowStyle.icon, []], + iconPosition: [computedTimewindowStyle.iconPosition, []], + font: [computedTimewindowStyle.font, []], + color: [computedTimewindowStyle.color, []], + displayTypePrefix: [computedTimewindowStyle.displayTypePrefix, []] + } + ); + this.updatePreviewStyle(this.timewindowStyle); + this.updateTimewindowStyleEnabledState(); + this.timewindowStyleFormGroup.valueChanges.subscribe((timewindowStyle: TimewindowStyle) => { + if (this.timewindowStyleFormGroup.valid) { + this.updatePreviewStyle(timewindowStyle); + setTimeout(() => {this.popover?.updatePosition();}, 0); + } + }); + this.timewindowStyleFormGroup.get('showIcon').valueChanges.subscribe(() => { + this.updateTimewindowStyleEnabledState(); + }); + } + + cancel() { + this.popover?.hide(); + } + + applyTimewindowStyle() { + const timewindowStyle = this.timewindowStyleFormGroup.getRawValue(); + this.timewindowStyleApplied.emit(timewindowStyle); + } + + private updateTimewindowStyleEnabledState() { + const showIcon: boolean = this.timewindowStyleFormGroup.get('showIcon').value; + if (showIcon) { + this.timewindowStyleFormGroup.get('iconSize').enable({emitEvent: false}); + this.timewindowStyleFormGroup.get('icon').enable({emitEvent: false}); + this.timewindowStyleFormGroup.get('iconPosition').enable({emitEvent: false}); + } else { + this.timewindowStyleFormGroup.get('iconSize').disable({emitEvent: false}); + this.timewindowStyleFormGroup.get('icon').disable({emitEvent: false}); + this.timewindowStyleFormGroup.get('iconPosition').disable({emitEvent: false}); + } + } + + private updatePreviewStyle(timewindowStyle: TimewindowStyle) { + this.previewTimewindowStyle = deepClone(timewindowStyle); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.html b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.html new file mode 100644 index 0000000000..2bd981fffc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.html @@ -0,0 +1,25 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.ts new file mode 100644 index 0000000000..ad6d3e1b85 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2023 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, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TimewindowStyle } from '@shared/models/widget-settings.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { Timewindow } from '@shared/models/time/time.models'; +import { TimewindowStylePanelComponent } from '@home/components/widget/config/timewindow-style-panel.component'; + +@Component({ + selector: 'tb-timewindow-style', + templateUrl: './timewindow-style.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimewindowStyleComponent), + multi: true + } + ] +}) +export class TimewindowStyleComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + previewValue: Timewindow; + + private modelValue: TimewindowStyle; + + private propagateChange = null; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: TimewindowStyle): void { + this.modelValue = value; + } + + openTimewindowStylePopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + timewindowStyle: this.modelValue, + previewValue: this.previewValue + }; + const timewindowStylePanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, TimewindowStylePanelComponent, 'left', true, null, + ctx, + {}, + {}, {}, true); + timewindowStylePanelPopover.tbComponentRef.instance.popover = timewindowStylePanelPopover; + timewindowStylePanelPopover.tbComponentRef.instance.timewindowStyleApplied.subscribe((timewindowStyle) => { + timewindowStylePanelPopover.hide(); + this.modelValue = timewindowStyle; + this.propagateChange(this.modelValue); + }); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts index 3777973eff..ea0d27dcf6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts @@ -29,6 +29,9 @@ import { FilterSelectComponent } from '@home/components/filter/filter-select.com import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module'; import { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component'; import { TimewindowConfigPanelComponent } from '@home/components/widget/config/timewindow-config-panel.component'; +import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module'; +import { TimewindowStyleComponent } from '@home/components/widget/config/timewindow-style.component'; +import { TimewindowStylePanelComponent } from '@home/components/widget/config/timewindow-style-panel.component'; @NgModule({ declarations: @@ -42,13 +45,16 @@ import { TimewindowConfigPanelComponent } from '@home/components/widget/config/t DatasourcesComponent, EntityAliasSelectComponent, FilterSelectComponent, + TimewindowStyleComponent, + TimewindowStylePanelComponent, TimewindowConfigPanelComponent, WidgetSettingsComponent ], imports: [ CommonModule, SharedModule, - WidgetSettingsModule + WidgetSettingsModule, + WidgetSettingsCommonModule ], exports: [ AlarmAssigneeSelectComponent, @@ -60,8 +66,11 @@ import { TimewindowConfigPanelComponent } from '@home/components/widget/config/t DatasourcesComponent, EntityAliasSelectComponent, FilterSelectComponent, + TimewindowStyleComponent, + TimewindowStylePanelComponent, TimewindowConfigPanelComponent, - WidgetSettingsComponent + WidgetSettingsComponent, + WidgetSettingsCommonModule ] }) export class WidgetConfigComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts index 9bda9345dc..75112d7cfa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts @@ -119,7 +119,7 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement return this.configForm().valid; } - protected setupDefaultDatasource(configData: WidgetConfigComponentData, keys?: DataKey[]) { + protected setupDefaultDatasource(configData: WidgetConfigComponentData, keys?: DataKey[], latestKeys?: DataKey[]) { let datasources = configData.config.datasources; if (!datasources || !datasources.length) { datasources = [ @@ -135,23 +135,61 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement dataKeys = []; datasources[0].dataKeys = dataKeys; } + let latestDataKeys = datasources[0].latestDataKeys; + if (!latestDataKeys) { + latestDataKeys = []; + datasources[0].latestDataKeys = latestDataKeys; + } if (keys && keys.length) { dataKeys.length = 0; keys.forEach(key => { - const dataKey = - this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema); - if (key.label) { - dataKey.label = key.label; - } - if (key.units) { - dataKey.units = key.units; - } - if (isDefinedAndNotNull(key.decimals)) { - dataKey.decimals = key.decimals; - } + const dataKey = this.constructDataKey(configData, key); dataKeys.push(dataKey); }); } + if (latestKeys && latestKeys.length) { + latestDataKeys.length = 0; + latestKeys.forEach(key => { + const dataKey = this.constructDataKey(configData, key); + latestDataKeys.push(dataKey); + }); + } + } + + protected constructDataKey(configData: WidgetConfigComponentData, key: DataKey): DataKey { + const dataKey = + this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema); + if (key.label) { + dataKey.label = key.label; + } + if (key.units) { + dataKey.units = key.units; + } + if (isDefinedAndNotNull(key.decimals)) { + dataKey.decimals = key.decimals; + } + if (key.color) { + dataKey.color = key.color; + } + if (isDefinedAndNotNull(key.settings)) { + dataKey.settings = key.settings; + } + if (isDefinedAndNotNull(key.aggregationType)) { + dataKey.aggregationType = key.aggregationType; + } + if (isDefinedAndNotNull(key.comparisonEnabled)) { + dataKey.comparisonEnabled = key.comparisonEnabled; + } + if (isDefinedAndNotNull(key.timeForComparison)) { + dataKey.timeForComparison = key.timeForComparison; + } + if (isDefinedAndNotNull(key.comparisonCustomIntervalValue)) { + dataKey.comparisonCustomIntervalValue = key.comparisonCustomIntervalValue; + } + if (isDefinedAndNotNull(key.comparisonResultType)) { + dataKey.comparisonResultType = key.comparisonResultType; + } + return dataKey; } protected abstract configForm(): UntypedFormGroup; 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 cc482bfd4e..bf2144cdfd 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 @@ -44,6 +44,7 @@ import { widgetSettingsComponentsMap } from '@home/components/widget/lib/setting import { Dashboard } from '@shared/models/dashboard.models'; import { WidgetService } from '@core/http/widget.service'; import { IAliasController } from '@core/api/widget-api.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; @Component({ selector: 'tb-widget-settings', @@ -73,6 +74,9 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On @Input() widget: Widget; + @Input() + widgetConfig: WidgetConfigComponentData; + private settingsDirective: string; definedDirectiveError: string; @@ -126,6 +130,11 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On this.definedSettingsComponent.aliasController = this.aliasController; } } + if (propName === 'widgetConfig') { + if (this.definedSettingsComponent) { + this.definedSettingsComponent.widgetConfig = this.widgetConfig; + } + } } } } @@ -214,6 +223,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On this.definedSettingsComponent.aliasController = this.aliasController; this.definedSettingsComponent.dashboard = this.dashboard; this.definedSettingsComponent.widget = this.widget; + this.definedSettingsComponent.widgetConfig = this.widgetConfig; this.definedSettingsComponent.functionScopeVariables = this.widgetService.getWidgetScopeVariables(); this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { this.updateModel(settings); diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts index dc3e9c7ac2..3c20317b13 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts @@ -20,8 +20,8 @@ import { ComponentFactory, ComponentRef, HostBinding, Inject, - Injector, - OnDestroy, + Injector, NgModuleRef, + OnDestroy, Type, ViewContainerRef } from '@angular/core'; import { DialogComponent } from '@shared/components/dialog.component'; @@ -35,11 +35,13 @@ import { } from '@home/components/widget/dialog/custom-dialog.component'; import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; +import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service'; export interface CustomDialogContainerData { controller: (instance: CustomDialogComponent) => void; data?: any; - customComponentFactory: ComponentFactory; + customComponentType: Type; + customComponentModuleRef: NgModuleRef; } @Component({ @@ -77,7 +79,8 @@ export class CustomDialogContainerComponent extends DialogComponent> + private customModules: Array>; constructor( private translate: TranslateService, @@ -56,12 +56,13 @@ export class CustomDialogService { if (Array.isArray(this.customModules)) { modules.push(...this.customModules); } - return this.dynamicComponentFactoryService.createDynamicComponentFactory( + return this.dynamicComponentFactoryService.createDynamicComponent( class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe( - mergeMap((factory) => { + mergeMap((componentData) => { const dialogData: CustomDialogContainerData = { controller, - customComponentFactory: factory, + customComponentType: componentData.componentType, + customComponentModuleRef: componentData.componentModuleRef, data }; let dialogConfig: MatDialogConfig = { @@ -76,7 +77,7 @@ export class CustomDialogService { CustomDialogContainerComponent, dialogConfig).afterClosed().pipe( tap(() => { - this.dynamicComponentFactoryService.destroyDynamicComponentFactory(factory); + this.dynamicComponentFactoryService.destroyDynamicComponent(componentData.componentType); }) ); } diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts index 22fac750b5..feeb1fe802 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts @@ -15,7 +15,7 @@ /// import { PageComponent } from '@shared/components/page.component'; -import { Directive, Injector, OnDestroy, OnInit } from '@angular/core'; +import { Directive, Injector, OnDestroy, OnInit, TemplateRef } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { IDynamicWidgetComponent, WidgetContext } from '@home/models/widget-component.models'; @@ -67,7 +67,8 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid @TbInject(UntypedFormBuilder) public fb: UntypedFormBuilder, @TbInject(Injector) public readonly $injector: Injector, @TbInject('widgetContext') public readonly ctx: WidgetContext, - @TbInject('errorMessages') public readonly errorMessages: string[]) { + @TbInject('errorMessages') public readonly errorMessages: string[], + @TbInject('widgetTitlePanel') public readonly widgetTitlePanel: TemplateRef) { super(store); this.ctx.$injector = $injector; this.ctx.deviceService = $injector.get(DeviceService); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index 54f0a21083..5228fff081 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -27,7 +27,7 @@  
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html index 6eaf4ca205..75d60e046f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html @@ -22,7 +22,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html index 2d6ea9b3fa..52e6b54519 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html @@ -38,8 +38,8 @@
- - + +
@@ -47,7 +47,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html index 1ccc2e8b24..e4b56e491b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html @@ -23,7 +23,7 @@ matTooltipPosition="above" mat-icon-button (click)="edit()"> - edit + edit
@@ -32,7 +32,7 @@ [href]="docLink.link" target="_blank"> @@ -43,7 +43,7 @@ matTooltip="{{ 'widgets.documentation.add-link' | translate }}" matTooltipPosition="above" (click)="addLink()"> - add + add
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html index e70e8cd5c2..baddc057c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html @@ -22,7 +22,7 @@
@@ -60,7 +60,7 @@ matTooltip="{{ 'action.drag' | translate }}" matTooltipPosition="above" class="tb-drag-handle"> - drag_indicator + drag_indicator
@@ -71,7 +71,7 @@ matTooltip="{{ (mode === 'docs' ? 'widgets.documentation.add-link' : 'widgets.quick-links.add-link') | translate }}" matTooltipPosition="above" (click)="addLink()"> - add + add
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss index a488d77c6e..4adf70d97d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss @@ -23,6 +23,7 @@ letter-spacing: 0.2px; color: rgba(0, 0, 0, 0.76); .mat-icon { + vertical-align: bottom; margin-right: 10px; color: rgba(0, 0, 0, 0.54); font-size: 20px; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html index 0dec41e71d..da1e9977a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html @@ -25,21 +25,19 @@ (focusin)="onFocus()" required [matAutocomplete]="linkAutocomplete"> - {{ quickLink.icon }} - + {{ quickLink.icon }} - {{ link.icon }} - + {{ link.icon }} @@ -58,8 +56,8 @@
- - + +
@@ -67,8 +65,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html index 95c2818f1c..58d3d98ee3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html @@ -23,7 +23,7 @@ matTooltipPosition="above" mat-icon-button (click)="edit()"> - edit + edit
@@ -32,8 +32,7 @@ [routerLink]="quickLink.path"> @@ -44,7 +43,7 @@ matTooltip="{{ 'widgets.quick-links.add-link' | translate }}" matTooltipPosition="above" (click)="addLink()"> - add + add
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts index 20cce79457..2e9a9fb5f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts @@ -94,12 +94,8 @@ export class JsonInputWidgetComponent extends PageComponent implements OnInit { private initializeConfig() { if (this.settings.widgetTitle && this.settings.widgetTitle.length) { - const title = createLabelFromDatasource(this.datasource, this.settings.widgetTitle); - this.ctx.widgetTitle = this.utils.customTranslation(title, title); - } else { - this.ctx.widgetTitle = this.ctx.widgetConfig.title; + this.ctx.widgetTitle = this.settings.widgetTitle; } - if (this.settings.labelValue && this.settings.labelValue.length) { const label = createLabelFromDatasource(this.datasource, this.settings.labelValue); this.labelValue = this.utils.customTranslation(label, label); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss index ae61340873..a12903ff47 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss @@ -72,6 +72,7 @@ text-align: left; white-space: nowrap; outline: none; + cursor: pointer; &.tb-horizontal { width: 95%; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html index 0a757fd34f..ae739332be 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html @@ -23,12 +23,12 @@
{{ getGroupTitle(source.datasource) }} -
- + {{key.label}} - {{key.settings.icon}} + {{key.settings.icon}} icon @@ -50,7 +50,7 @@
- + {{key.label}} - {{key.settings.icon}} + {{key.settings.icon}} icon @@ -86,7 +86,7 @@
- + {{key.label}} - {{key.settings.icon}} + {{key.settings.icon}} icon @@ -123,7 +123,7 @@ [labelPosition]="key.settings.slideToggleLabelPosition" (change)="inputChanged(source, key)"> - {{key.settings.icon}} + {{key.settings.icon}} icon @@ -134,7 +134,7 @@
- + {{key.label}}
- + {{key.label}}
+ +
+
+ + {{key.settings.icon}} + + icon + + + {{key.label}} +
+ + +
-
-
-
+
+
{{ 'widgets.input-widgets.no-entity-selected' | translate }}
-
+
{{ 'widgets.input-widgets.not-allowed-entity' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss index d0dd324e52..846becbc6f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss @@ -21,14 +21,14 @@ flex-direction: column; .tb-multiple-input-container { - padding: 0 8px; + padding: 8px 8px 0; flex: 1 1 100%; overflow-x: hidden; overflow-y: auto; } .fields-group { - padding: 0 8px; + padding: 8px 0 0 8px; margin: 10px 0; border: 1px groove rgba(0, 0, 0, .25); @@ -37,6 +37,28 @@ } } + .tb-multiple-input-layout { + display: flex; + flex-direction: row; + align-items: start; + } + + .color-picker-input { + padding: 7px 16px 7px 12px; + margin: 0 10px 22px 0; + border-color: rgba(0, 0, 0, 0.4); + + .label-container { + display: flex; + flex-direction: row; + align-items: center; + } + + .mat-icon, img { + margin-right: 5px; + } + } + .input-field { padding-right: 10px; @@ -61,6 +83,30 @@ .vertical-alignment { flex-direction: column; } + + &--buttons-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: end; + &__button { + max-height: 50px; + margin-right:20px; + } + } + + &--errors-container { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + &__error { + text-align: center; + font-size: 18px; + color: #a0a0a0; + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts index 534be7c676..f667c785c1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts @@ -54,7 +54,7 @@ type FieldAlignment = 'row' | 'column'; type MultipleInputWidgetDataKeyType = 'server' | 'shared' | 'timeseries'; export type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' | 'JSON' | 'booleanCheckbox' | 'booleanSwitch' | - 'dateTime' | 'date' | 'time' | 'select'; + 'dateTime' | 'date' | 'time' | 'select' | 'color'; type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly'; type ConvertGetValueFunction = (value: any, ctx: WidgetContext) => any; @@ -202,10 +202,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni private initializeConfig() { if (this.settings.widgetTitle && this.settings.widgetTitle.length) { - const titlePatternText = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle); - this.ctx.widgetTitle = createLabelFromDatasource(this.datasources[0], titlePatternText); - } else { - this.ctx.widgetTitle = this.ctx.widgetConfig.title; + this.ctx.widgetTitle = this.settings.widgetTitle; } this.settings.groupTitle = this.settings.groupTitle || '${entityName}'; @@ -390,6 +387,12 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni } }); } + } else if (key.settings.dataKeyValueType === 'color') { + formControl.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => { + this.inputChanged(source, key); + }); } this.multipleInputFormGroup.addControl(key.formId, formControl); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html index 0d33c487b7..77da2cabbb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html @@ -16,6 +16,6 @@ --> - {{settings.icon}} + {{settings.icon}} {{translatedName}} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss index a04f82dce7..b9c3e034a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss @@ -31,7 +31,7 @@ display: flex; flex-direction: column; align-items: center; - mat-icon { + .mat-icon { margin: auto !important; } span.mdc-button__label { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html index dbd4d50981..d63a4ef76f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html @@ -25,8 +25,7 @@ - {{place.icon}} - + {{place.icon}} {{place.name}} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss index f3710fbc81..7d41450655 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss @@ -55,7 +55,7 @@ display: flex; flex-direction: column; align-items: center; - mat-icon { + .mat-icon { margin: auto; } span.mdc-button__label { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html index 93b0742930..66f19f717a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html @@ -80,7 +80,7 @@ matTooltip="{{ actionDescriptor.displayName }}" matTooltipPosition="above" (click)="onActionButtonClick($event, column, actionDescriptor)"> - {{ actionDescriptor.icon }} + {{ actionDescriptor.icon }}
@@ -88,14 +88,14 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html index 9baf6c998f..f408c6d93e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html @@ -62,8 +62,8 @@
- - + + {{ 'widgets.table.use-cell-style-function' | translate }} @@ -86,8 +86,8 @@
- - + + {{ 'widgets.table.use-cell-content-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html index 4ec8f31be6..e304def2c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html @@ -94,7 +94,7 @@ {{ 'widgets.table.display-pagination' | translate }} -
+
widgets.table.default-page-size
@@ -104,8 +104,8 @@
widgets.table.rows
- - + + {{ 'widgets.table.use-row-style-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html new file mode 100644 index 0000000000..32d48347ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html @@ -0,0 +1,48 @@ + + +
+
widgets.aggregated-value-card.value-appearance
+
+
{{ 'widgets.aggregated-value-card.position' | translate }}
+ + + + {{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }} + + + +
+
+
{{ 'widgets.aggregated-value-card.font' | translate }}
+ + +
+
+
{{ 'widgets.aggregated-value-card.color' | translate }}
+ + +
+
+ + {{ 'widgets.aggregated-value-card.display-up-down-arrow' | translate }} + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts new file mode 100644 index 0000000000..e527c3b3d4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts @@ -0,0 +1,64 @@ +/// +/// Copyright © 2016-2023 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 { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + aggregatedValueCardDefaultKeySettings, + AggregatedValueCardKeyPosition, + aggregatedValueCardKeyPositionTranslations +} from '@home/components/widget/lib/cards/aggregated-value-card.models'; +import { constantColor } from '@shared/models/widget-settings.models'; + +@Component({ + selector: 'tb-aggregated-value-card-key-settings', + templateUrl: './aggregated-value-card-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class AggregatedValueCardKeySettingsComponent extends WidgetSettingsComponent { + + aggregatedValueCardKeyPositions: AggregatedValueCardKeyPosition[] = + Object.keys(AggregatedValueCardKeyPosition).map(value => AggregatedValueCardKeyPosition[value]); + + aggregatedValueCardKeyPositionTranslationMap = aggregatedValueCardKeyPositionTranslations; + + aggregatedValueCardKeySettingsForm: UntypedFormGroup; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.aggregatedValueCardKeySettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...aggregatedValueCardDefaultKeySettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.aggregatedValueCardKeySettingsForm = this.fb.group({ + position: [settings.position, []], + font: [settings.font, []], + color: [settings.color, []], + showArrow: [settings.showArrow, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.html new file mode 100644 index 0000000000..c4af456487 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.html @@ -0,0 +1,65 @@ + + +
+
widgets.aggregated-value-card.aggregated-value-card-style
+
+ + {{ 'widgets.aggregated-value-card.subtitle' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.value-card.date' | translate }} + +
+ + + + + +
+
+
+ + {{ 'widgets.aggregated-value-card.chart' | translate }} + +
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts new file mode 100644 index 0000000000..bb2fda0b50 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts @@ -0,0 +1,111 @@ +/// +/// Copyright © 2016-2023 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, Injector } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-settings.models'; +import { aggregatedValueCardDefaultSettings } from '@home/components/widget/lib/cards/aggregated-value-card.models'; + +@Component({ + selector: 'tb-aggregated-value-card-widget-settings', + templateUrl: './aggregated-value-card-widget-settings.component.html', + styleUrls: [] +}) +export class AggregatedValueCardWidgetSettingsComponent extends WidgetSettingsComponent { + + aggregatedValueCardWidgetSettingsForm: UntypedFormGroup; + + datePreviewFn = this._datePreviewFn.bind(this); + + constructor(protected store: Store, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.aggregatedValueCardWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...aggregatedValueCardDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.aggregatedValueCardWidgetSettingsForm = this.fb.group({ + + showSubtitle: [settings.showSubtitle, []], + subtitle: [settings.subtitle, []], + subtitleFont: [settings.subtitleFont, []], + subtitleColor: [settings.subtitleColor, []], + + showDate: [settings.showDate, []], + dateFormat: [settings.dateFormat, []], + dateFont: [settings.dateFont, []], + dateColor: [settings.dateColor, []], + + showChart: [settings.showChart, []], + + background: [settings.background, []] + }); + } + + protected validatorTriggers(): string[] { + return ['showSubtitle', 'showDate']; + } + + protected updateValidators(emitEvent: boolean) { + const showSubtitle: boolean = this.aggregatedValueCardWidgetSettingsForm.get('showSubtitle').value; + const showDate: boolean = this.aggregatedValueCardWidgetSettingsForm.get('showDate').value; + + if (showSubtitle) { + this.aggregatedValueCardWidgetSettingsForm.get('subtitle').enable(); + this.aggregatedValueCardWidgetSettingsForm.get('subtitleFont').enable(); + this.aggregatedValueCardWidgetSettingsForm.get('subtitleColor').enable(); + } else { + this.aggregatedValueCardWidgetSettingsForm.get('subtitle').disable(); + this.aggregatedValueCardWidgetSettingsForm.get('subtitleFont').disable(); + this.aggregatedValueCardWidgetSettingsForm.get('subtitleColor').disable(); + } + + if (showDate) { + this.aggregatedValueCardWidgetSettingsForm.get('dateFormat').enable(); + this.aggregatedValueCardWidgetSettingsForm.get('dateFont').enable(); + this.aggregatedValueCardWidgetSettingsForm.get('dateColor').enable(); + } else { + this.aggregatedValueCardWidgetSettingsForm.get('dateFormat').disable(); + this.aggregatedValueCardWidgetSettingsForm.get('dateFont').disable(); + this.aggregatedValueCardWidgetSettingsForm.get('dateColor').disable(); + } + + this.aggregatedValueCardWidgetSettingsForm.get('subtitle').updateValueAndValidity({emitEvent}); + this.aggregatedValueCardWidgetSettingsForm.get('subtitleFont').updateValueAndValidity({emitEvent}); + this.aggregatedValueCardWidgetSettingsForm.get('subtitleColor').updateValueAndValidity({emitEvent}); + this.aggregatedValueCardWidgetSettingsForm.get('dateFormat').updateValueAndValidity({emitEvent}); + this.aggregatedValueCardWidgetSettingsForm.get('dateFont').updateValueAndValidity({emitEvent}); + this.aggregatedValueCardWidgetSettingsForm.get('dateColor').updateValueAndValidity({emitEvent}); + } + + private _datePreviewFn(): string { + const dateFormat: DateFormatSettings = this.aggregatedValueCardWidgetSettingsForm.get('dateFormat').value; + const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); + processor.update(Date.now()); + return processor.formatted; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html index 02d51ee325..faad357026 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html @@ -62,8 +62,8 @@
- - + + {{ 'widgets.table.use-cell-style-function' | translate }} @@ -86,8 +86,8 @@
- - + + {{ 'widgets.table.use-cell-content-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html index f8867b9f6a..f7afe557ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html @@ -41,7 +41,7 @@
widgets.table.columns
-
+
{{ 'widgets.table.display-entity-name' | translate }} @@ -49,8 +49,8 @@
-
- +
+ {{ 'widgets.table.display-entity-label' | translate }} @@ -91,7 +91,7 @@ {{ 'widgets.table.display-pagination' | translate }} -
+
widgets.table.default-page-size
@@ -101,8 +101,8 @@
widgets.table.rows
- - + + {{ 'widgets.table.use-row-style-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html index 6c79eed622..165f4eb0f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html @@ -50,8 +50,8 @@
- - + + {{ 'widgets.table.use-cell-style-function' | translate }} @@ -74,8 +74,8 @@
- - + + {{ 'widgets.table.use-cell-content-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html index 97cbe28ae9..fd34b66af6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html @@ -59,8 +59,8 @@
- - + + {{ 'widgets.table.use-cell-style-function' | translate }} @@ -83,8 +83,8 @@
- - + + {{ 'widgets.table.use-cell-content-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html index 170d460f92..d70ae9964b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html @@ -59,7 +59,7 @@ {{ 'widgets.table.display-pagination' | translate }} -
+
widgets.table.default-page-size
@@ -78,8 +78,8 @@ {{ 'widgets.table.hide-empty-lines' | translate }} - - + + {{ 'widgets.table.use-row-style-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html new file mode 100644 index 0000000000..f0fd9cdb16 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html @@ -0,0 +1,90 @@ + + +
+
widgets.value-card.value-card-style
+ + + {{ valueCardLayoutTranslationMap.get(layout) | translate }} + + +
+ + {{ 'widgets.value-card.label' | translate }} + +
+ + + + +
+
+
+ + {{ 'widgets.value-card.icon' | translate }} + +
+ + + + + + + + +
+
+
+
widgets.value-card.value
+
+ + + + +
+
+
+ + {{ 'widgets.value-card.date' | translate }} + +
+ + + + + +
+
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts new file mode 100644 index 0000000000..ad50dde8d2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts @@ -0,0 +1,200 @@ +/// +/// Copyright © 2016-2023 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, Injector } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + valueCardDefaultSettings, + ValueCardLayout, valueCardLayoutImages, + valueCardLayouts, valueCardLayoutTranslations +} from '@home/components/widget/lib/cards/value-card-widget.models'; +import { formatValue, isDefinedAndNotNull } from '@core/utils'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { + DateFormatProcessor, + DateFormatSettings, + getLabel +} from '@shared/models/widget-settings.models'; + +@Component({ + selector: 'tb-value-card-widget-settings', + templateUrl: './value-card-widget-settings.component.html', + styleUrls: [] +}) +export class ValueCardWidgetSettingsComponent extends WidgetSettingsComponent { + + valueCardLayouts: ValueCardLayout[] = []; + + valueCardLayoutTranslationMap = valueCardLayoutTranslations; + valueCardLayoutImageMap = valueCardLayoutImages; + + horizontal = false; + + valueCardWidgetSettingsForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + datePreviewFn = this._datePreviewFn.bind(this); + + + get label(): string { + return getLabel(this.widgetConfig.config.datasources); + } + + get dateEnabled(): boolean { + const layout: ValueCardLayout = this.valueCardWidgetSettingsForm.get('layout').value; + return ![ValueCardLayout.vertical, ValueCardLayout.simplified].includes(layout); + } + + get iconEnabled(): boolean { + const layout: ValueCardLayout = this.valueCardWidgetSettingsForm.get('layout').value; + return layout !== ValueCardLayout.simplified; + } + + constructor(protected store: Store, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.valueCardWidgetSettingsForm; + } + + protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) { + const params = widgetConfig.typeParameters as any; + this.horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false; + this.valueCardLayouts = valueCardLayouts(this.horizontal); + } + + protected defaultSettings(): WidgetSettings { + return valueCardDefaultSettings(this.horizontal); + } + + protected onSettingsSet(settings: WidgetSettings) { + this.valueCardWidgetSettingsForm = this.fb.group({ + layout: [settings.layout, []], + + showLabel: [settings.showLabel, []], + labelFont: [settings.labelFont, []], + labelColor: [settings.labelColor, []], + + showIcon: [settings.showIcon, []], + iconSize: [settings.iconSize, [Validators.min(0)]], + iconSizeUnit: [settings.iconSizeUnit, []], + icon: [settings.icon, []], + iconColor: [settings.iconColor, []], + + valueFont: [settings.valueFont, []], + valueColor: [settings.valueColor, []], + + showDate: [settings.showDate, []], + dateFormat: [settings.dateFormat, []], + dateFont: [settings.dateFont, []], + dateColor: [settings.dateColor, []], + + background: [settings.background, []] + }); + } + + protected validatorTriggers(): string[] { + return ['layout', 'showLabel', 'showIcon', 'showDate']; + } + + protected updateValidators(emitEvent: boolean) { + const layout: ValueCardLayout = this.valueCardWidgetSettingsForm.get('layout').value; + const showLabel: boolean = this.valueCardWidgetSettingsForm.get('showLabel').value; + const showIcon: boolean = this.valueCardWidgetSettingsForm.get('showIcon').value; + const showDate: boolean = this.valueCardWidgetSettingsForm.get('showDate').value; + + const dateEnabled = ![ValueCardLayout.vertical, ValueCardLayout.simplified].includes(layout); + const iconEnabled = layout !== ValueCardLayout.simplified; + + if (showLabel) { + this.valueCardWidgetSettingsForm.get('labelFont').enable(); + this.valueCardWidgetSettingsForm.get('labelColor').enable(); + } else { + this.valueCardWidgetSettingsForm.get('labelFont').disable(); + this.valueCardWidgetSettingsForm.get('labelColor').disable(); + } + + if (iconEnabled) { + this.valueCardWidgetSettingsForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.valueCardWidgetSettingsForm.get('iconSize').enable(); + this.valueCardWidgetSettingsForm.get('iconSizeUnit').enable(); + this.valueCardWidgetSettingsForm.get('icon').enable(); + this.valueCardWidgetSettingsForm.get('iconColor').enable(); + } else { + this.valueCardWidgetSettingsForm.get('iconSize').disable(); + this.valueCardWidgetSettingsForm.get('iconSizeUnit').disable(); + this.valueCardWidgetSettingsForm.get('icon').disable(); + this.valueCardWidgetSettingsForm.get('iconColor').disable(); + } + } else { + this.valueCardWidgetSettingsForm.get('showIcon').disable({emitEvent: false}); + this.valueCardWidgetSettingsForm.get('iconSize').disable(); + this.valueCardWidgetSettingsForm.get('iconSizeUnit').disable(); + this.valueCardWidgetSettingsForm.get('icon').disable(); + this.valueCardWidgetSettingsForm.get('iconColor').disable(); + } + + if (dateEnabled) { + this.valueCardWidgetSettingsForm.get('showDate').enable({emitEvent: false}); + if (showDate) { + this.valueCardWidgetSettingsForm.get('dateFormat').enable(); + this.valueCardWidgetSettingsForm.get('dateFont').enable(); + this.valueCardWidgetSettingsForm.get('dateColor').enable(); + } else { + this.valueCardWidgetSettingsForm.get('dateFormat').disable(); + this.valueCardWidgetSettingsForm.get('dateFont').disable(); + this.valueCardWidgetSettingsForm.get('dateColor').disable(); + } + } else { + this.valueCardWidgetSettingsForm.get('showDate').disable({emitEvent: false}); + this.valueCardWidgetSettingsForm.get('dateFormat').disable(); + this.valueCardWidgetSettingsForm.get('dateFont').disable(); + this.valueCardWidgetSettingsForm.get('dateColor').disable(); + } + this.valueCardWidgetSettingsForm.get('showIcon').updateValueAndValidity({emitEvent: false}); + this.valueCardWidgetSettingsForm.get('showDate').updateValueAndValidity({emitEvent: false}); + this.valueCardWidgetSettingsForm.get('labelFont').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('labelColor').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('iconSize').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('iconSizeUnit').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('icon').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('iconColor').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('dateFormat').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('dateFont').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('dateColor').updateValueAndValidity({emitEvent}); + } + + private _valuePreviewFn(): string { + const units: string = this.widgetConfig.config.units; + const decimals: number = this.widgetConfig.config.decimals; + return formatValue(22, decimals, units, true); + } + + private _datePreviewFn(): string { + const dateFormat: DateFormatSettings = this.valueCardWidgetSettingsForm.get('dateFormat').value; + const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); + processor.update(Date.now()); + return processor.formatted; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html index 96ff4d8559..a424b61606 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html @@ -32,8 +32,8 @@
- - + + {{ 'widgets.chart.show-line' | translate }} @@ -65,8 +65,8 @@
- - + + {{ 'widgets.chart.show-points' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html index 307a494954..3d5e608ad2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html @@ -21,15 +21,24 @@
widgets.chart.radius - + + + {{ 'widgets.chart.common-pie-settings-range-error' | translate }} + widgets.chart.inner-radius - + + + {{ 'widgets.chart.common-pie-settings-range-error' | translate }} + widgets.chart.tilt - + + + {{ 'widgets.chart.common-pie-settings-range-error' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.ts index 9b18ace983..0627af3581 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.ts @@ -59,9 +59,9 @@ export class FlotPieWidgetSettingsComponent extends WidgetSettingsComponent { // Common pie settings - radius: [settings.radius, [Validators.min(0)]], - innerRadius: [settings.innerRadius, [Validators.min(0)]], - tilt: [settings.tilt, [Validators.min(0)]], + radius: [settings.radius, [Validators.min(0), Validators.max(1)]], + innerRadius: [settings.innerRadius, [Validators.min(0), Validators.max(1)]], + tilt: [settings.tilt, [Validators.min(0), Validators.max(1)]], // Stroke settings diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html index 6f08b5f3f7..be2ae39255 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html @@ -17,7 +17,7 @@ -->
- +
{{ thresholdText() }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss index 150d516894..81f5d17bc6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss @@ -29,6 +29,9 @@ display: flex; flex-direction: row; align-items: stretch; + .mat-content { + overflow: hidden; + } .tb-threshold-header { flex: 1; display: flex; @@ -36,6 +39,7 @@ gap: 16px; align-items: center; padding-left: 16px; + overflow: hidden; .mat-divider-vertical { height: 100%; } @@ -47,6 +51,9 @@ font-weight: 400; line-height: 16px; letter-spacing: 0.15px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } .mat-expansion-indicator { margin-right: 22px; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html index 5dd7eaa2dc..4ea648c3b4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html @@ -77,8 +77,8 @@
widget-config.legend
- - + + {{ 'widget-config.legend' | translate }} @@ -119,8 +119,8 @@
widgets.chart.ticks
- - + + {{ 'widgets.chart.ticks' | translate }} @@ -171,8 +171,8 @@
widgets.chart.ticks
- - + + {{ 'widgets.chart.ticks' | translate }} @@ -234,8 +234,8 @@
widgets.chart.tooltip
- - + + {{ 'widgets.chart.tooltip' | translate }} @@ -274,8 +274,8 @@
widgets.chart.comparison-settings
- - + + {{ 'widgets.chart.enable-comparison' | translate }} @@ -350,8 +350,8 @@
widgets.chart.custom-legend-settings
- - + + {{ 'widgets.chart.enable-custom-legend' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts index 46454937f6..9241f60dda 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts @@ -74,7 +74,8 @@ export const flotDefaultSettings = (chartType: ChartType): Partial +
+
widgets.background.background-settings
+
+
+
widgets.background.background
+ + + {{ backgroundTypeTranslationsMap.get(type) | translate }} + + +
+ +
+
widgets.background.image-url
+ + + +
+
+
widgets.color.color
+ + +
+
+
+
widgets.background.overlay
+ + {{ 'widgets.background.enable-overlay' | translate }} + +
+
widgets.color.color
+ + +
+
+
widgets.background.blur
+ + +
px
+
+
+
+
+
+ widgets.background.preview +
+
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.scss new file mode 100644 index 0000000000..258117512a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.scss @@ -0,0 +1,73 @@ +/** + * Copyright © 2016-2023 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-background-settings-panel { + width: 620px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-background-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-background-settings-preview { + flex: 1; + background: rgba(0, 0, 0, 0.04); + display: flex; + flex-direction: column; + padding: 12px 16px 24px 16px; + align-items: center; + gap: 12px; + } + .tb-background-settings-preview-title { + align-self: stretch; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; + color: rgba(0, 0, 0, 0.38); + } + .tb-background-settings-preview-box { + position: relative; + width: 136px; + height: 118px; + border-radius: 2.666px; + } + .tb-background-settings-preview-overlay { + position: absolute; + border-radius: 2.666px; + top: 7.998px; + bottom: 7.998px; + left: 7.998px; + right: 7.998px; + } + .tb-background-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/components/widget/lib/settings/common/background-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts new file mode 100644 index 0000000000..ab94b95800 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts @@ -0,0 +1,120 @@ +/// +/// Copyright © 2016-2023 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 { PageComponent } from '@shared/components/page.component'; +import { + backgroundStyle, + overlayStyle, + BackgroundSettings, + BackgroundType, + backgroundTypeTranslations, ComponentStyle +} from '@shared/models/widget-settings.models'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-background-settings-panel', + templateUrl: './background-settings-panel.component.html', + providers: [], + styleUrls: ['./background-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class BackgroundSettingsPanelComponent extends PageComponent implements OnInit { + + @Input() + backgroundSettings: BackgroundSettings; + + @Input() + popover: TbPopoverComponent; + + @Output() + backgroundSettingsApplied = new EventEmitter(); + + backgroundType = BackgroundType; + + backgroundTypes = Object.keys(BackgroundType) as BackgroundType[]; + + backgroundTypeTranslationsMap = backgroundTypeTranslations; + + backgroundSettingsFormGroup: UntypedFormGroup; + + backgroundStyle: ComponentStyle = {}; + overlayStyle: ComponentStyle = {}; + + constructor(private fb: UntypedFormBuilder, + protected store: Store) { + super(store); + } + + ngOnInit(): void { + this.backgroundSettingsFormGroup = this.fb.group( + { + type: [this.backgroundSettings?.type, []], + imageBase64: [this.backgroundSettings?.imageBase64, []], + imageUrl: [this.backgroundSettings?.imageUrl, []], + color: [this.backgroundSettings?.color, []], + overlay: this.fb.group({ + enabled: [this.backgroundSettings?.overlay?.enabled, []], + color: [this.backgroundSettings?.overlay?.color, []], + blur: [this.backgroundSettings?.overlay?.blur, []] + }) + } + ); + this.backgroundSettingsFormGroup.get('type').valueChanges.subscribe(() => { + setTimeout(() => {this.popover?.updatePosition();}, 0); + }); + this.backgroundSettingsFormGroup.get('overlay').get('enabled').valueChanges.subscribe(() => { + this.updateValidators(); + }); + this.backgroundSettingsFormGroup.valueChanges.subscribe(() => { + this.updateBackgroundStyle(); + }); + this.updateValidators(); + this.updateBackgroundStyle(); + } + + cancel() { + this.popover?.hide(); + } + + applyColorSettings() { + const backgroundSettings = this.backgroundSettingsFormGroup.getRawValue(); + this.backgroundSettingsApplied.emit(backgroundSettings); + } + + private updateValidators() { + const overlayEnabled: boolean = this.backgroundSettingsFormGroup.get('overlay').get('enabled').value; + if (overlayEnabled) { + this.backgroundSettingsFormGroup.get('overlay').get('color').enable(); + this.backgroundSettingsFormGroup.get('overlay').get('blur').enable(); + } else { + this.backgroundSettingsFormGroup.get('overlay').get('color').disable(); + this.backgroundSettingsFormGroup.get('overlay').get('blur').disable(); + } + this.backgroundSettingsFormGroup.get('overlay').get('color').updateValueAndValidity({emitEvent: false}); + this.backgroundSettingsFormGroup.get('overlay').get('blur').updateValueAndValidity({emitEvent: false}); + } + + private updateBackgroundStyle() { + const background: BackgroundSettings = this.backgroundSettingsFormGroup.value; + this.backgroundStyle = backgroundStyle(background); + this.overlayStyle = overlayStyle(background.overlay); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.html new file mode 100644 index 0000000000..e9e1b99b0e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.html @@ -0,0 +1,30 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.scss new file mode 100644 index 0000000000..6f73fbffa4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.scss @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2023 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. + */ +button.mat-mdc-button-base.tb-box-button.tb-background-settings { + padding: 0; + .mat-mdc-button-persistent-ripple { + z-index: 2; + } + .tb-color-preview { + width: 38px; + min-width: 38px; + height: 38px; + &.box { + .tb-color-result { + &:after { + border: none; + } + } + .tb-color-overlay { + position: absolute; + border-radius: 3px; + top: 4px; + bottom: 4px; + left: 4px; + right: 4px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts new file mode 100644 index 0000000000..62a2495942 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts @@ -0,0 +1,120 @@ +/// +/// Copyright © 2016-2023 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, ViewContainerRef, ViewEncapsulation } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { + BackgroundSettings, + backgroundStyle, + BackgroundType, + ComponentStyle, + overlayStyle +} from '@shared/models/widget-settings.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { + BackgroundSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/background-settings-panel.component'; + +@Component({ + selector: 'tb-background-settings', + templateUrl: './background-settings.component.html', + styleUrls: ['./background-settings.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => BackgroundSettingsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class BackgroundSettingsComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + backgroundType = BackgroundType; + + modelValue: BackgroundSettings; + + backgroundStyle: ComponentStyle = {}; + + overlayStyle: ComponentStyle = {}; + + private propagateChange = null; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + this.updateBackgroundStyle(); + } + + writeValue(value: BackgroundSettings): void { + this.modelValue = value; + this.updateBackgroundStyle(); + } + + openBackgroundSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + backgroundSettings: this.modelValue + }; + const backgroundSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, BackgroundSettingsPanelComponent, 'left', true, null, + ctx, + {}, + {}, {}, true); + backgroundSettingsPanelPopover.tbComponentRef.instance.popover = backgroundSettingsPanelPopover; + backgroundSettingsPanelPopover.tbComponentRef.instance.backgroundSettingsApplied.subscribe((backgroundSettings) => { + backgroundSettingsPanelPopover.hide(); + this.modelValue = backgroundSettings; + this.updateBackgroundStyle(); + this.propagateChange(this.modelValue); + }); + } + } + + private updateBackgroundStyle() { + if (!this.disabled) { + this.backgroundStyle = backgroundStyle(this.modelValue); + this.overlayStyle = overlayStyle(this.modelValue.overlay); + } else { + this.backgroundStyle = {}; + this.overlayStyle = {}; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html new file mode 100644 index 0000000000..cd8cf1bc06 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html @@ -0,0 +1,105 @@ + +
+
widgets.color.color-settings
+
+ + + {{ colorTypeTranslationsMap.get(type) | translate }} + + +
+
+
widgets.color.color
+ + +
+
+
+
+ +
+
+ +
+
+ + +
+
+ + +
+
widgets.color.value-range
+
+
+
+
+
widgets.color.from
+ + + +
widgets.color.to
+ + + + + +
+ +
+
+
+ +
+
+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss new file mode 100644 index 0000000000..0036723b92 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2023 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-color-settings-panel { + width: 500px; + height: 470px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-xs} { + width: 90vw; + } + .tb-color-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-color-ranges-panel { + flex: 1; + min-height: 0; + gap: 16px; + display: flex; + flex-direction: column; + } + .tb-color-ranges { + flex: 1; + gap: 12px; + display: flex; + flex-direction: column; + overflow: auto; + } + .tb-form-row { + height: auto; + .tb-value-range-text { + width: 64px; + font-size: 14px; + color: rgba(0, 0, 0, 0.38); + @media #{$mat-xs} { + width: auto; + } + &.tb-value-range-text-to { + text-align: center; + } + } + } + button.mat-mdc-button-base.tb-add-color-range { + &:not(:disabled) { + color: rgba(0, 0, 0, 0.54); + } + &:disabled { + color: rgba(0, 0, 0, 0.12); + } + } + .tb-color-settings-panel-body { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + } + .tb-color-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/components/widget/lib/settings/common/color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts new file mode 100644 index 0000000000..483674437d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts @@ -0,0 +1,133 @@ +/// +/// Copyright © 2016-2023 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 { PageComponent } from '@shared/components/page.component'; +import { + ColorRange, + ColorSettings, + ColorType, + colorTypeTranslations +} from '@shared/models/widget-settings.models'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { + AbstractControl, + FormControl, + FormGroup, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Datasource, DatasourceType } from '@shared/models/widget.models'; +import { deepClone } from '@core/utils'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-color-settings-panel', + templateUrl: './color-settings-panel.component.html', + providers: [], + styleUrls: ['./color-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ColorSettingsPanelComponent extends PageComponent implements OnInit { + + @Input() + colorSettings: ColorSettings; + + @Input() + popover: TbPopoverComponent; + + @Output() + colorSettingsApplied = new EventEmitter(); + + colorType = ColorType; + + colorTypes = Object.keys(ColorType) as ColorType[]; + + colorTypeTranslationsMap = colorTypeTranslations; + + colorSettingsFormGroup: UntypedFormGroup; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService, + protected store: Store) { + super(store); + } + + ngOnInit(): void { + this.colorSettingsFormGroup = this.fb.group( + { + type: [this.colorSettings?.type, []], + color: [this.colorSettings?.color, []], + rangeList: this.fb.array((this.colorSettings?.rangeList || []).map(r => this.colorRangeControl(r))), + colorFunction: [this.colorSettings?.colorFunction, []] + } + ); + this.colorSettingsFormGroup.get('type').valueChanges.subscribe(() => { + setTimeout(() => {this.popover?.updatePosition();}, 0); + }); + } + + private colorRangeControl(range: ColorRange): AbstractControl { + return this.fb.group({ + from: [range?.from, []], + to: [range?.to, []], + color: [range?.color, []] + }); + } + + get rangeListFormArray(): UntypedFormArray { + return this.colorSettingsFormGroup.get('rangeList') as UntypedFormArray; + } + + get rangeListFormGroups(): FormGroup[] { + return this.rangeListFormArray.controls as FormGroup[]; + } + + trackByRange(index: number, rangeControl: AbstractControl): any { + return rangeControl; + } + + removeRange(index: number) { + this.rangeListFormArray.removeAt(index); + this.colorSettingsFormGroup.markAsDirty(); + setTimeout(() => {this.popover?.updatePosition();}, 0); + } + + addRange() { + const newRange: ColorRange = { + color: 'rgba(0,0,0,0.87)' + }; + this.rangeListFormArray.push(this.colorRangeControl(newRange)); + this.colorSettingsFormGroup.markAsDirty(); + setTimeout(() => {this.popover?.updatePosition();}, 0); + } + + cancel() { + this.popover?.hide(); + } + + applyColorSettings() { + const colorSettings = this.colorSettingsFormGroup.value; + this.colorSettingsApplied.emit(colorSettings); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html new file mode 100644 index 0000000000..b5e26cc9ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html @@ -0,0 +1,30 @@ + + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts new file mode 100644 index 0000000000..f92203459e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts @@ -0,0 +1,124 @@ +/// +/// Copyright © 2016-2023 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, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ColorSettings, ColorType, ComponentStyle } from '@shared/models/widget-settings.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { + ColorSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/color-settings-panel.component'; + +@Component({ + selector: 'tb-color-settings', + templateUrl: './color-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ColorSettingsComponent), + multi: true + } + ] +}) +export class ColorSettingsComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + colorType = ColorType; + + modelValue: ColorSettings; + + colorStyle: ComponentStyle = {}; + + private propagateChange = null; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + this.updateColorStyle(); + } + + writeValue(value: ColorSettings): void { + this.modelValue = value; + this.updateColorStyle(); + } + + openColorSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + colorSettings: this.modelValue + }; + const colorSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, ColorSettingsPanelComponent, 'left', true, null, + ctx, + {}, + {}, {}, true); + colorSettingsPanelPopover.tbComponentRef.instance.popover = colorSettingsPanelPopover; + colorSettingsPanelPopover.tbComponentRef.instance.colorSettingsApplied.subscribe((colorSettings) => { + colorSettingsPanelPopover.hide(); + this.modelValue = colorSettings; + this.updateColorStyle(); + this.propagateChange(this.modelValue); + }); + } + } + + private updateColorStyle() { + if (!this.disabled && this.modelValue) { + let colors: string[] = [this.modelValue.color]; + if (this.modelValue.type === ColorType.range && this.modelValue.rangeList?.length) { + const rangeColors = this.modelValue.rangeList.slice(0, Math.min(2, this.modelValue.rangeList.length)).map(r => r.color); + colors = colors.concat(rangeColors); + } + if (colors.length === 1) { + this.colorStyle = {backgroundColor: colors[0]}; + } else { + const gradientValues: string[] = []; + const step = 100 / colors.length; + for (let i = 0; i < colors.length; i++) { + gradientValues.push(`${colors[i]} ${step*i}%`); + gradientValues.push(`${colors[i]} ${step*(i+1)}%`); + } + this.colorStyle = {background: `linear-gradient(90deg, ${gradientValues.join(', ')})`}; + } + } else { + this.colorStyle = {}; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html new file mode 100644 index 0000000000..1c00141b1d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html @@ -0,0 +1,24 @@ + + + + + + {{ cssUnit }} + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts new file mode 100644 index 0000000000..61a6bc4865 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts @@ -0,0 +1,87 @@ +/// +/// Copyright © 2016-2023 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 } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; +import { cssUnit, cssUnits } from '@shared/models/widget-settings.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-css-unit-select', + templateUrl: './css-unit-select.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CssUnitSelectComponent), + multi: true + } + ] +}) +export class CssUnitSelectComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + allowEmpty = false; + + cssUnitsList = cssUnits; + + cssUnitFormControl: UntypedFormControl; + + modelValue: cssUnit; + + private propagateChange = null; + + constructor() {} + + ngOnInit(): void { + this.cssUnitFormControl = new UntypedFormControl(); + this.cssUnitFormControl.valueChanges.subscribe((value: cssUnit) => { + this.updateModel(value); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.cssUnitFormControl.disable({emitEvent: false}); + } else { + this.cssUnitFormControl.enable({emitEvent: false}); + } + } + + writeValue(value: cssUnit): void { + this.modelValue = value; + this.cssUnitFormControl.patchValue(this.modelValue, {emitEvent: false}); + } + + updateModel(value: cssUnit): void { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html new file mode 100644 index 0000000000..7310d0c403 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html @@ -0,0 +1,26 @@ + + + + {{ dateFormatDisplayValue(dateFormat) }} + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts new file mode 100644 index 0000000000..e4f0b47946 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts @@ -0,0 +1,148 @@ +/// +/// Copyright © 2016-2023 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 } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; +import { + compareDateFormats, + dateFormats, + DateFormatSettings +} from '@shared/models/widget-settings.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { deepClone } from '@core/utils'; +import { + DateFormatSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/date-format-settings-panel.component'; + +@Component({ + selector: 'tb-date-format-select', + templateUrl: './date-format-select.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DateFormatSelectComponent), + multi: true + } + ] +}) +export class DateFormatSelectComponent implements OnInit, ControlValueAccessor { + + @ViewChild('customFormatButton', {static: false}) + customFormatButton: MatButton; + + @Input() + disabled: boolean; + + dateFormatList = dateFormats; + + dateFormatsCompare = compareDateFormats; + + dateFormatFormControl: UntypedFormControl; + + modelValue: DateFormatSettings; + + private propagateChange = null; + + private formatCache: {[format: string]: string} = {}; + + constructor(private translate: TranslateService, + private date: DatePipe, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + this.dateFormatFormControl = new UntypedFormControl(); + this.dateFormatFormControl.valueChanges.subscribe((value: DateFormatSettings) => { + this.updateModel(value); + if (value?.custom) { + setTimeout(() => { + this.openDateFormatSettingsPopup(null, this.customFormatButton); + }, 0); + } + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.dateFormatFormControl.disable({emitEvent: false}); + } else { + this.dateFormatFormControl.enable({emitEvent: false}); + } + } + + writeValue(value: DateFormatSettings): void { + this.modelValue = value; + this.dateFormatFormControl.patchValue(this.modelValue, {emitEvent: false}); + } + + updateModel(value: DateFormatSettings): void { + if (!compareDateFormats(this.modelValue, value)) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + dateFormatDisplayValue(value: DateFormatSettings): string { + if (value.custom) { + return this.translate.instant('date.custom-date'); + } else if (value.lastUpdateAgo) { + return this.translate.instant('date.last-update-n-ago'); + } else { + if (!this.formatCache[value.format]) { + this.formatCache[value.format] = this.date.transform(Date.now(), value.format); + } + return this.formatCache[value.format]; + } + } + + openDateFormatSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + dateFormat: deepClone(this.modelValue) + }; + const dateFormatSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, DateFormatSettingsPanelComponent, 'top', true, null, + ctx, + {}, + {}, {}, true); + dateFormatSettingsPanelPopover.tbComponentRef.instance.popover = dateFormatSettingsPanelPopover; + dateFormatSettingsPanelPopover.tbComponentRef.instance.dateFormatApplied.subscribe((dateFormat) => { + dateFormatSettingsPanelPopover.hide(); + this.modelValue = dateFormat; + this.propagateChange(this.modelValue); + }); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.html new file mode 100644 index 0000000000..ee332924f0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.html @@ -0,0 +1,47 @@ + +
+
date.custom-date
+
+
date.format
+ + +
+
+
+ +
+
date.preview
+
{{ previewText }}
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.scss new file mode 100644 index 0000000000..ec6a762966 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.scss @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2023 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-date-format-settings-panel { + width: 500px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-xs} { + width: 90vw; + } + .tb-date-format-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-form-row { + .fixed-title-width { + min-width: 120px; + } + &.date-format-preview { + align-items: flex-start; + .preview-text { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.38); + } + } + .mat-mdc-form-field.tb-date-format-input { + .mat-mdc-text-field-wrapper.mdc-text-field--outlined { + .mat-mdc-form-field-icon-suffix { + display: flex; + align-items: center; + line-height: normal; + } + } + } + } + .tb-date-format-settings-panel-buttons { + height: 60px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts new file mode 100644 index 0000000000..be120b80f6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts @@ -0,0 +1,70 @@ +/// +/// Copyright © 2016-2023 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 { PageComponent } from '@shared/components/page.component'; +import { DateFormatSettings } from '@shared/models/widget-settings.models'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormControl, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DatePipe } from '@angular/common'; + +@Component({ + selector: 'tb-date-format-settings-panel', + templateUrl: './date-format-settings-panel.component.html', + providers: [], + styleUrls: ['./date-format-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class DateFormatSettingsPanelComponent extends PageComponent implements OnInit { + + @Input() + dateFormat: DateFormatSettings; + + @Input() + popover: TbPopoverComponent; + + @Output() + dateFormatApplied = new EventEmitter(); + + dateFormatFormControl: UntypedFormControl; + + previewText = ''; + + constructor(private date: DatePipe, + protected store: Store) { + super(store); + } + + ngOnInit(): void { + this.dateFormatFormControl = new UntypedFormControl(this.dateFormat.format, [Validators.required]); + this.dateFormatFormControl.valueChanges.subscribe((value: string) => { + this.previewText = this.date.transform(Date.now(), value); + }); + this.previewText = this.date.transform(Date.now(), this.dateFormat.format); + } + + cancel() { + this.popover?.hide(); + } + + applyDateFormat() { + this.dateFormat.format = this.dateFormatFormControl.value; + this.dateFormatApplied.emit(this.dateFormat); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html new file mode 100644 index 0000000000..9fe3273f74 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html @@ -0,0 +1,109 @@ + +
+
widgets.widget-font.font-settings
+
+
widgets.widget-font.size
+
+ + + + +
+
+
+
widgets.widget-font.font-family
+ + + + + + + + + +
+
+
widgets.widget-font.font-weight
+ + + + + + {{ fontWeightTranslationsMap.has(weight) ? (fontWeightTranslationsMap.get(weight) | translate) : weight }} + + + +
+
+
widgets.widget-font.font-style
+ + + + + + {{ fontStyleTranslationsMap.get(style) | translate }} + + + +
+
+
widgets.widget-font.line-height
+ + + +
+ +
+
widgets.widget-font.preview
+
{{ previewText }}
+
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.scss new file mode 100644 index 0000000000..3475a14e8c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.scss @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2023 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-font-settings-panel { + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + .tb-font-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-form-row { + .fixed-title-width { + min-width: 120px; + } + &.font-preview { + align-items: flex-start; + .preview-text { + max-height: 300px; + max-width: 400px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + } + .tb-font-settings-panel-buttons { + height: 60px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts new file mode 100644 index 0000000000..a0e77ef380 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts @@ -0,0 +1,153 @@ +/// +/// Copyright © 2016-2023 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, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { + commonFonts, + ComponentStyle, + Font, + fontStyles, + fontStyleTranslations, + fontWeights, + fontWeightTranslations, isFontPartiallySet, + textStyle +} from '@shared/models/widget-settings.models'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Observable } from 'rxjs'; +import { map, startWith, tap } from 'rxjs/operators'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-font-settings-panel', + templateUrl: './font-settings-panel.component.html', + providers: [], + styleUrls: ['./font-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class FontSettingsPanelComponent extends PageComponent implements OnInit { + + @Input() + font: Font; + + @Input() + previewText = 'AaBbCcDd'; + + @Input() + initialPreviewStyle: ComponentStyle; + + @Input() + @coerceBoolean() + clearButton = false; + + @Input() + popover: TbPopoverComponent; + + @Output() + fontApplied = new EventEmitter(); + + @ViewChild('familyInput', {static: true}) familyInput: ElementRef; + + fontWeightsList = fontWeights; + + fontWeightTranslationsMap = fontWeightTranslations; + + fontStylesList = fontStyles; + + fontStyleTranslationsMap = fontStyleTranslations; + + fontFormGroup: UntypedFormGroup; + + filteredFontFamilies: Observable>; + + familySearchText = ''; + + previewStyle: ComponentStyle = {}; + + constructor(private fb: UntypedFormBuilder, + protected store: Store) { + super(store); + } + + ngOnInit(): void { + this.fontFormGroup = this.fb.group( + { + size: [this.font?.size, [Validators.min(0)]], + sizeUnit: [(this.font?.sizeUnit || 'px'), []], + family: [this.font?.family, []], + weight: [this.font?.weight, []], + style: [this.font?.style, []], + lineHeight: [this.font?.lineHeight, []] + } + ); + this.updatePreviewStyle(this.font); + this.fontFormGroup.valueChanges.subscribe((font: Font) => { + if (this.fontFormGroup.valid) { + this.updatePreviewStyle(font); + setTimeout(() => {this.popover?.updatePosition();}, 0); + } + }); + this.filteredFontFamilies = this.fontFormGroup.get('family').valueChanges + .pipe( + startWith(''), + tap((searchText) => { this.familySearchText = searchText || ''; }), + map(() => commonFonts.filter(f => f.toUpperCase().includes(this.familySearchText.toUpperCase()))) + ); + } + + clearFamily() { + this.fontFormGroup.get('family').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.familyInput.nativeElement.blur(); + this.familyInput.nativeElement.focus(); + }, 0); + } + + cancel() { + this.popover?.hide(); + } + + applyFont() { + const font = this.fontFormGroup.value; + this.fontApplied.emit(font); + } + + clearDisabled(): boolean { + return !isFontPartiallySet(this.fontFormGroup.value); + } + + clearFont() { + this.fontFormGroup.reset({sizeUnit: 'px'}); + this.fontFormGroup.markAsDirty(); + } + + private updatePreviewStyle(font: Font) { + this.previewStyle = {...(this.initialPreviewStyle || {}), ...textStyle(font)}; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.html new file mode 100644 index 0000000000..574ce98c1d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.html @@ -0,0 +1,25 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts new file mode 100644 index 0000000000..f23862cf34 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts @@ -0,0 +1,112 @@ +/// +/// Copyright © 2016-2023 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, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ComponentStyle, Font } from '@shared/models/widget-settings.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { FontSettingsPanelComponent } from '@home/components/widget/lib/settings/common/font-settings-panel.component'; +import { isDefinedAndNotNull } from '@core/utils'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-font-settings', + templateUrl: './font-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FontSettingsComponent), + multi: true + } + ] +}) +export class FontSettingsComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + previewText: string | (() => string); + + @Input() + initialPreviewStyle: ComponentStyle; + + @Input() + @coerceBoolean() + clearButton = false; + + private modelValue: Font; + + private propagateChange = null; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: Font): void { + this.modelValue = value; + } + + openFontSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + font: this.modelValue, + initialPreviewStyle: this.initialPreviewStyle, + clearButton: this.clearButton + }; + if (isDefinedAndNotNull(this.previewText)) { + const previewText = typeof this.previewText === 'string' ? this.previewText : this.previewText(); + if (previewText) { + ctx.previewText = previewText; + } + } + const fontSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, FontSettingsPanelComponent, 'left', true, null, + ctx, + {}, + {}, {}, true); + fontSettingsPanelPopover.tbComponentRef.instance.popover = fontSettingsPanelPopover; + fontSettingsPanelPopover.tbComponentRef.instance.fontApplied.subscribe((font) => { + fontSettingsPanelPopover.hide(); + this.modelValue = font; + this.propagateChange(this.modelValue); + }); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html new file mode 100644 index 0000000000..52bd5907bd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html @@ -0,0 +1,43 @@ + +
+ + +
+
{{ label }}
+ + + {{ expanded ? 'expand_less' : 'expand_more' }} + +
+
+ + + +
+
+
{{ option.name }}
+
+ +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.scss new file mode 100644 index 0000000000..37c06ed239 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.scss @@ -0,0 +1,116 @@ +/** + * Copyright © 2016-2023 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-image-cards-select.tb-form-panel { + .tb-form-row { + transition: all .3s; + &.expanded { + padding: 11px 7px 11px 16px; + } + } + .tb-image-cards-value-field { + cursor: pointer; + user-select: none; + input { + cursor: pointer; + pointer-events: none; + } + } + .mat-expansion-panel { + &.tb-settings { + > .mat-expansion-panel-header { + height: auto; + .mat-content { + margin: 0; + } + .tb-form-row { + font-weight: normal; + font-size: 16px; + color: rgba(0, 0, 0, 0.87); + } + .mat-expansion-indicator { + display: none; + } + } + > .mat-expansion-panel-content { + > .mat-expansion-panel-body { + padding: 0 16px 16px !important; + } + } + } + } + .tb-image-cards-option { + width: 100%; + height: 100%; + cursor: pointer; + padding: 8px 12px 12px 12px; + display: flex; + flex-direction: column; + gap: 8px; + align-items: start; + position: relative; + .tb-image-cards-option-background { + border-radius: 4px; + position: absolute; + top: 1px; + left: 1px; + right: 1px; + bottom: 1px; + background: rgba(0, 0, 0, 0.04); + } + &:before { + content: unset; + border-radius: 4px; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + .tb-image-cards-option-title { + z-index: 1; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.54); + } + .tb-image-cards-option-image-container { + z-index: 1; + flex: 1; + width: 100%; + min-height: 0; + display: flex; + justify-content: center; + } + &.selected { + .tb-image-cards-option-background { + background: #305680; + opacity: 0.04; + } + &:before { + content: ""; + border: 1px solid #305680; + opacity: 0.32; + } + .tb-image-cards-option-title { + font-size: 13px; + font-weight: 500; + color: #305680; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts new file mode 100644 index 0000000000..d75c4a5fc9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts @@ -0,0 +1,203 @@ +/// +/// Copyright © 2016-2023 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 { + AfterContentInit, + Component, + ContentChildren, + Directive, + ElementRef, + forwardRef, + Input, OnChanges, + OnDestroy, OnInit, + QueryList, SimpleChanges, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'; +import { map, share, startWith, takeUntil } from 'rxjs/operators'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; + +export interface ImageCardsSelectOption { + name: string; + value: any; + image: string; +} + +@Directive( + { + // eslint-disable-next-line @angular-eslint/directive-selector + selector: 'tb-image-cards-select-option', + } +) +export class ImageCardsSelectOptionDirective { + + @Input() value: any; + + @Input() image: string; + + get viewValue(): string { + return (this._element?.nativeElement.textContent || '').trim(); + } + + constructor( + private _element: ElementRef + ) {} +} + +@Component({ + selector: 'tb-image-cards-select', + templateUrl: './image-cards-select.component.html', + styleUrls: ['./image-cards-select.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ImageCardsSelectComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit, OnChanges, AfterContentInit, OnDestroy { + + @ContentChildren(ImageCardsSelectOptionDirective) imageCardsSelectOptions: QueryList; + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + cols = 4; + + @Input() + colsLtMd = 2; + + @Input() + rowHeight = '9:5'; + + @Input() + label: string; + + valueFormControl: UntypedFormControl; + + options: ImageCardsSelectOption[] = []; + + modelValue: any; + + expanded = false; + + cols$: Observable; + + private propagateChange = null; + + private _destroyed = new Subject(); + + private _colsChanged = new BehaviorSubject(null); + + constructor(private breakpointObserver: BreakpointObserver) { + this.valueFormControl = new UntypedFormControl(''); + } + + ngOnInit(): void { + const gridColumns = this.breakpointObserver.isMatched(MediaBreakpoints['lt-md']) ? this.colsLtMd : this.cols; + this.cols$ = combineLatest({state: this.breakpointObserver + .observe(MediaBreakpoints['lt-md']), colsChanged: this._colsChanged.asObservable()}).pipe( + map((data) => data.state.matches ? this.colsLtMd : this.cols), + startWith(gridColumns), + share() + ); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['cols', 'colsLtMd'].includes(propName)) { + this._colsChanged.next(null); + } + } + } + } + + ngAfterContentInit(): void { + this.imageCardsSelectOptions.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { + this.syncImageCardsSelectOptions(); + }); + } + + ngOnDestroy() { + this._destroyed.next(); + this._destroyed.complete(); + } + + private syncImageCardsSelectOptions() { + if (this.imageCardsSelectOptions?.length) { + this.options.length = 0; + this.imageCardsSelectOptions.forEach(option => { + this.options.push( + { name: option.viewValue, + value: option.value, + image: option.image + } + ); + }); + this.updateDisplayValue(); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.valueFormControl.disable(); + } else { + this.valueFormControl.enable(); + } + } + + writeValue(value: any): void { + this.modelValue = value; + this.updateDisplayValue(); + } + + updateModel(value: any) { + this.modelValue = value; + this.updateDisplayValue(); + this.propagateChange(this.modelValue); + this.expanded = false; + } + + toggleSelectPanel($event: Event) { + $event.stopPropagation(); + if (!this.disabled) { + this.expanded = !this.expanded; + } + } + + private updateDisplayValue() { + const currentOption = this.options.find(o => o.value === this.modelValue); + const displayValue = currentOption ? currentOption.name : ''; + this.valueFormControl.patchValue(displayValue, {emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html index 10bfa058da..349bc0cd04 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html @@ -38,7 +38,7 @@
-
+
legend.show-values
{{ 'legend.min-option' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html index 3cc771e94f..726aed776e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html @@ -17,7 +17,7 @@ -->
- + {{ 'widgets.value-source.predefined-value' | translate }} @@ -32,7 +32,7 @@
-
+
widgets.value-source.source-entity-alias
-
+
widgets.value-source.source-entity-attribute
{{ 'widgets.input-widgets.datakey-value-type-json' | translate }} + + {{ 'widgets.input-widgets.datakey-value-type-color' | translate }} + 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 2dae32a922..34b4712a6f 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 @@ -32,7 +32,6 @@ import { import { MarkdownWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/markdown-widget-settings.component'; -import { WidgetFontComponent } from '@home/components/widget/lib/settings/common/widget-font.component'; import { LabelWidgetLabelComponent } from '@home/components/widget/lib/settings/cards/label-widget-label.component'; import { LabelWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/label-widget-settings.component'; import { @@ -72,7 +71,6 @@ import { import { DigitalGaugeWidgetSettingsComponent } from '@home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component'; -import { ValueSourceComponent } from '@home/components/widget/lib/settings/common/value-source.component'; import { FixedColorLevelComponent } from '@home/components/widget/lib/settings/gauge/fixed-color-level.component'; import { TickValueComponent } from '@home/components/widget/lib/settings/gauge/tick-value.component'; import { FlotWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/flot-widget-settings.component'; @@ -265,7 +263,16 @@ import { import { QuickLinksWidgetSettingsComponent } from '@home/components/widget/lib/settings/home-page/quick-links-widget-settings.component'; -import { LegendConfigComponent } from '@home/components/widget/lib/settings/common/legend-config.component'; +import { + ValueCardWidgetSettingsComponent +} from '@home/components/widget/lib/settings/cards/value-card-widget-settings.component'; +import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module'; +import { + AggregatedValueCardKeySettingsComponent +} from '@home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component'; +import { + AggregatedValueCardWidgetSettingsComponent +} from '@home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component'; @NgModule({ declarations: [ @@ -274,7 +281,6 @@ import { LegendConfigComponent } from '@home/components/widget/lib/settings/comm TimeseriesTableKeySettingsComponent, TimeseriesTableLatestKeySettingsComponent, MarkdownWidgetSettingsComponent, - WidgetFontComponent, LabelWidgetLabelComponent, LabelWidgetSettingsComponent, SimpleCardWidgetSettingsComponent, @@ -290,8 +296,6 @@ import { LegendConfigComponent } from '@home/components/widget/lib/settings/comm AnalogueLinearGaugeWidgetSettingsComponent, AnalogueCompassWidgetSettingsComponent, DigitalGaugeWidgetSettingsComponent, - ValueSourceComponent, - LegendConfigComponent, FixedColorLevelComponent, TickValueComponent, FlotWidgetSettingsComponent, @@ -367,12 +371,16 @@ import { LegendConfigComponent } from '@home/components/widget/lib/settings/comm RouteMapWidgetSettingsComponent, TripAnimationWidgetSettingsComponent, DocLinksWidgetSettingsComponent, - QuickLinksWidgetSettingsComponent + QuickLinksWidgetSettingsComponent, + ValueCardWidgetSettingsComponent, + AggregatedValueCardKeySettingsComponent, + AggregatedValueCardWidgetSettingsComponent ], imports: [ CommonModule, SharedModule, - SharedHomeComponentsModule + SharedHomeComponentsModule, + WidgetSettingsCommonModule ], exports: [ QrCodeWidgetSettingsComponent, @@ -380,7 +388,6 @@ import { LegendConfigComponent } from '@home/components/widget/lib/settings/comm TimeseriesTableKeySettingsComponent, TimeseriesTableLatestKeySettingsComponent, MarkdownWidgetSettingsComponent, - WidgetFontComponent, LabelWidgetLabelComponent, LabelWidgetSettingsComponent, SimpleCardWidgetSettingsComponent, @@ -396,8 +403,6 @@ import { LegendConfigComponent } from '@home/components/widget/lib/settings/comm AnalogueLinearGaugeWidgetSettingsComponent, AnalogueCompassWidgetSettingsComponent, DigitalGaugeWidgetSettingsComponent, - ValueSourceComponent, - LegendConfigComponent, FixedColorLevelComponent, TickValueComponent, FlotWidgetSettingsComponent, @@ -473,7 +478,10 @@ import { LegendConfigComponent } from '@home/components/widget/lib/settings/comm RouteMapWidgetSettingsComponent, TripAnimationWidgetSettingsComponent, DocLinksWidgetSettingsComponent, - QuickLinksWidgetSettingsComponent + QuickLinksWidgetSettingsComponent, + ValueCardWidgetSettingsComponent, + AggregatedValueCardKeySettingsComponent, + AggregatedValueCardWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -542,5 +550,8 @@ export const widgetSettingsComponentsMap: {[key: string]: Type  
@@ -38,9 +40,20 @@
widget-config.title
- - - +
+ + + + + + + +
widget-config.title-tooltip
@@ -48,19 +61,21 @@
-
+
{{ 'widget-config.display-icon' | translate }}
+ + + - - -
@@ -86,12 +101,14 @@
{{ 'widget-config.text-color' | translate }}
{{ 'widget-config.background-color' | translate }}
@@ -247,7 +264,7 @@
widget-config.limits
-
+
widget-config.data-page-size
@@ -259,18 +276,18 @@
widget-config.data-settings
-
widget-config.units
+
widget-config.units-by-default
-
widget-config.decimals
+
widget-config.decimals-by-default
-
+
widget-config.no-data-display-message
@@ -282,6 +299,7 @@ [aliasController]="aliasController" [dashboard]="dashboard" [widget]="widget" + [widgetConfig]="modelValue" formControlName="settings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index 91b3368ad0..701bfec099 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -20,16 +20,36 @@ .tb-widget-config { display: flex; flex-direction: column; - gap: 16px; + gap: 8px; .tb-widget-config-header { - padding: 24px 24px 8px; - height: 56px; + padding: 24px 24px 0; display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; + 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-widget-config-header-components { + width: 100%; + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + } } .tb-widget-config-content { + & > .mat-content { + padding-top: 8px; + @media #{$mat-xs} { + padding-left: 8px; + padding-right: 8px; + } + } flex: 1; overflow: auto; & > div { @@ -39,6 +59,9 @@ display: flex; flex-direction: column; gap: 16px; + @media #{$mat-xs} { + gap: 8px; + } } } .tb-basic-mode-directive-error { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 1f418224dd..be02dd86c5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -21,10 +21,8 @@ import { ComponentRef, forwardRef, Input, - OnChanges, OnDestroy, OnInit, - SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core'; @@ -110,7 +108,7 @@ const defaultSettingsForm = [ } ] }) -export class WidgetConfigComponent extends PageComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator, OnChanges { +export class WidgetConfigComponent extends PageComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { @ViewChild('basicModeContainer', {read: ViewContainerRef, static: false}) basicModeContainer: ViewContainerRef; @@ -149,7 +147,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe @Input() disabled: boolean; - @Input() widgetConfigMode = WidgetConfigMode.advanced; widgetType: widgetType; @@ -212,6 +209,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe this.advancedSettings = this.fb.group({}); this.widgetSettings = this.fb.group({ title: [null, []], + titleFont: [null, []], + titleColor: [null, []], showTitleIcon: [null, []], titleIcon: [null, []], iconColor: [null, []], @@ -252,19 +251,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe }); } - ngOnChanges(changes: SimpleChanges): void { - for (const propName of Object.keys(changes)) { - const change = changes[propName]; - if (!change.firstChange && change.currentValue !== change.previousValue) { - if (propName === 'widgetConfigMode') { - if (this.hasBasicModeDirective) { - this.setupConfig(); - } - } - } - } - } - ngOnDestroy(): void { this.destroyBasicModeComponent(); this.removeChangeSubscriptions(); @@ -367,7 +353,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe this.dataSettings.addControl('timewindowConfig', this.fb.control({ useDashboardTimewindow: true, displayTimewindow: true, - timewindow: null + timewindow: null, + timewindowStyle: null })); if (this.widgetType === widgetType.alarm) { this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null)); @@ -404,9 +391,22 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe writeValue(value: WidgetConfigComponentData): void { this.modelValue = value; this.widgetType = this.modelValue?.widgetType; + this.widgetConfigMode = this.modelValue?.hasBasicMode ? + (this.modelValue?.config?.configMode || WidgetConfigMode.advanced) : WidgetConfigMode.advanced; this.setupConfig(this.isAdd); } + setWidgetConfigMode(widgetConfigMode: WidgetConfigMode) { + if (this.modelValue?.hasBasicMode && this.widgetConfigMode !== widgetConfigMode) { + this.widgetConfigMode = widgetConfigMode; + this.modelValue.config.configMode = widgetConfigMode; + if (this.hasBasicModeDirective) { + this.setupConfig(); + } + this.propagateChange(this.modelValue); + } + } + private setupConfig(isAdd = false) { if (this.modelValue) { this.destroyBasicModeComponent(); @@ -473,6 +473,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe const displayWidgetTitle = isDefined(config.showTitle) ? config.showTitle : false; this.widgetSettings.patchValue({ title: config.title, + titleFont: config.titleFont, + titleColor: config.titleColor, showTitleIcon: isDefined(config.showTitleIcon) && displayWidgetTitle ? config.showTitleIcon : false, titleIcon: isDefined(config.titleIcon) ? config.titleIcon : '', iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)', @@ -513,7 +515,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe useDashboardTimewindow, displayTimewindow: isDefined(config.displayTimewindow) ? config.displayTimewindow : true, - timewindow: config.timewindow + timewindow: config.timewindow, + timewindowStyle: config.timewindowStyle }, {emitEvent: false}); } if (this.modelValue.isDataEnabled) { @@ -580,11 +583,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value; if (showTitle) { this.widgetSettings.get('title').enable({emitEvent: false}); + this.widgetSettings.get('titleFont').enable({emitEvent: false}); + this.widgetSettings.get('titleColor').enable({emitEvent: false}); this.widgetSettings.get('titleTooltip').enable({emitEvent: false}); this.widgetSettings.get('titleStyle').enable({emitEvent: false}); this.widgetSettings.get('showTitleIcon').enable({emitEvent: false}); } else { this.widgetSettings.get('title').disable({emitEvent: false}); + this.widgetSettings.get('titleFont').disable({emitEvent: false}); + this.widgetSettings.get('titleColor').disable({emitEvent: false}); this.widgetSettings.get('titleTooltip').disable({emitEvent: false}); this.widgetSettings.get('titleStyle').disable({emitEvent: false}); this.widgetSettings.get('showTitleIcon').disable({emitEvent: 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 23082a121b..1e3fd33171 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 @@ -19,7 +19,6 @@ [fullscreenBackgroundStyle]="dashboardStyle" [fullscreenBackgroundImage]="backgroundImage" (fullscreenChanged)="onFullscreenChanged($event)" - fxLayout="column" class="tb-widget" [ngClass]="{ 'tb-highlighted': isHighlighted(widget), @@ -32,85 +31,93 @@ (mousedown)="onMouseDown($event)" (click)="onClicked($event)" (contextmenu)="onContextMenu($event)"> -
-
- - {{widget.titleIcon}} - {{widget.customTranslatedTitle}} - - - -
+
+ + +
-
- + + [isMobile]="isMobile" + [widgetTitlePanel]="widgetTitlePanel">
+ +
+
+ {{widget.titleIcon}} +
+ {{widget.title$ | async}} +
+
+ + +
+
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 4c7264ce29..a61dc463db 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 @@ -14,9 +14,14 @@ * limitations under the License. */ -tb-widget.tb-widget { - position: relative; - height: 100%; +.tb-widget-container { + position: absolute; + inset: 0; +} + +.tb-widget { + position: absolute; + inset: 0; margin: 0; overflow: hidden; outline: none; @@ -25,15 +30,20 @@ tb-widget.tb-widget { } div.tb-widget { - position: relative; - height: 100%; - margin: 0; - overflow: hidden; - outline: none; - - transition: all .2s ease-in-out; + display: flex; + flex-direction: column; + .tb-widget-header { + display: flex; + flex-direction: row; + place-content: flex-start space-between; + align-items: flex-start; + } .tb-widget-title { + display: flex; + flex-direction: column; + place-content: flex-start center; + align-items: flex-start; max-height: 65px; padding-top: 5px; padding-left: 5px; @@ -42,27 +52,32 @@ div.tb-widget { tb-timewindow { font-size: 14px; opacity: .85; - margin: 0; } - .title { + .title-row { + display: flex; + flex-direction: row; + place-content: center flex-start; + align-items: center; + gap: 4px; width: 100%; + } + + .title { overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; line-height: 24px; - letter-spacing: .01em; + letter-spacing: normal; margin: 0; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - - &.single-row{ - -webkit-line-clamp: 1; - } } } .tb-widget-actions { + display: flex; + flex-direction: row; + place-content: center flex-start; + align-items: center; z-index: 19; margin: 5px 0 0; @@ -82,7 +97,7 @@ div.tb-widget { margin: 0 !important; line-height: 20px; - mat-icon { + .mat-icon { width: 20px; min-width: 20px; height: 20px; @@ -104,13 +119,11 @@ div.tb-widget { } .tb-widget-content { + flex: 1; + position: relative; &.tb-no-interaction { pointer-events: none; } - tb-widget { - position: relative; - width: 100%; - } } &.tb-highlighted { 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 bf7042d233..c022015c8f 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 @@ -15,6 +15,7 @@ /// import { + AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -62,7 +63,7 @@ export class WidgetComponentAction { encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush }) -export class WidgetContainerComponent extends PageComponent implements OnInit, OnDestroy { +export class WidgetContainerComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { @HostBinding('class') widgetContainerClass = 'tb-widget-container'; @@ -131,6 +132,10 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O } } + ngAfterViewInit(): void { + this.widget.widgetContext.$widgetElement = $(this.tbWidgetElement.nativeElement); + } + ngOnDestroy(): void { if (this.cssClass) { const el = this.document.getElementById(this.cssClass); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html index 9b6c683953..74ba76bae1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html @@ -16,6 +16,8 @@ --> ) { @@ -71,13 +77,15 @@ export class WidgetPreviewComponent extends PageComponent implements OnInit, OnC } private loadPreviewWidget() { - const widget = deepClone(this.widget); - widget.sizeX = 24; - widget.sizeY = this.widget.sizeY * 2; - widget.row = 0; - widget.col = 0; - widget.config = this.widgetConfig; - this.widgets = [widget]; + if (this.widget) { + const widget = deepClone(this.widget); + widget.sizeX = 24; + widget.sizeY = this.widget.sizeY * 2; + widget.row = 0; + widget.col = 0; + widget.config = this.widgetConfig; + this.widgets = [widget]; + } } } 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 7e9b9cb6e7..96f39f0ee5 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 @@ -32,6 +32,7 @@ import { Optional, Renderer2, SimpleChanges, + TemplateRef, Type, ViewChild, ViewContainerRef, @@ -125,6 +126,9 @@ import { IModulesMap } from '@modules/common/modules-map.models'; }) export class WidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy { + @Input() + widgetTitlePanel: TemplateRef; + @Input() isEdit: boolean; @@ -297,7 +301,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.subscriptionContext.widgetUtils = this.widgetContext.utils; this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService); - this.widgetComponentService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe( + this.widgetComponentService.getWidgetInfo(this.widget.typeFullFqn).subscribe( (widgetInfo) => { this.widgetInfo = widgetInfo; this.loadFromWidgetInfo(); @@ -308,9 +312,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.loadFromWidgetInfo(); } ); - setTimeout(() => { - this.dashboardWidget.updateWidgetParams(); - }, 0); const noDataDisplayMessage = this.widget.config.noDataDisplayMessage; if (isNotEmptyStr(noDataDisplayMessage)) { @@ -387,7 +388,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.handleWidgetException(e); } } - this.widgetContext.destroyed = true; + this.widgetContext.destroy(); this.destroyDynamicWidgetComponent(); } } @@ -403,12 +404,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI private loadFromWidgetInfo() { this.widgetContext.widgetNamespace = - `widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`; + `widget-type-${this.widget.typeFullFqn.replace(/\./g, '-')}`; const elem = this.elementRef.nativeElement; elem.classList.add('tb-widget'); elem.classList.add(this.widgetContext.widgetNamespace); this.widgetType = this.widgetInfo.widgetTypeFunction; this.typeParameters = this.widgetInfo.typeParameters; + this.widgetContext.embedTitlePanel = this.typeParameters.embedTitlePanel; if (!this.widgetType) { this.widgetTypeInstance = {}; @@ -479,6 +481,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } if (!this.widgetContext.inited && this.isReady()) { this.widgetContext.inited = true; + this.dashboardWidget.updateWidgetParams(); this.widgetContext.detectContainerChanges(); if (this.cafs.init) { this.cafs.init(); @@ -492,7 +495,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI if (this.dataUpdatePending) { this.widgetTypeInstance.onDataUpdated(); setTimeout(() => { - this.dashboardWidget.updateCustomHeaderActions(true); + this.dashboardWidget.updateParamsFromData(true); }, 0); this.dataUpdatePending = false; } @@ -734,6 +737,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI { provide: 'errorMessages', useValue: this.errorMessages + }, + { + provide: 'widgetTitlePanel', + useValue: this.widgetTitlePanel } ], parent: this.injector @@ -744,7 +751,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.widgetContext.$containerParent = $(containerElement); try { - this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector); + this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentType, + {index: 0, injector, ngModuleRef: this.widgetInfo.componentModuleRef}); this.cd.detectChanges(); } catch (e) { if (this.dynamicWidgetComponentRef) { @@ -753,8 +761,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } this.widgetContentContainer.clear(); this.handleWidgetException(e); - this.widgetComponentService.clearWidgetInfo(this.widgetInfo, this.widget.bundleAlias, this.widget.typeAlias, - this.widget.isSystemType); + this.widgetComponentService.clearWidgetInfo(this.widgetInfo); throw e; } @@ -844,7 +851,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI if (this.widgetInstanceInited) { this.widgetTypeInstance.onDataUpdated(); setTimeout(() => { - this.dashboardWidget.updateCustomHeaderActions(true); + this.dashboardWidget.updateParamsFromData(true); }, 0); } else { this.dataUpdatePending = true; diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index 8d55b4bc6a..fd427923f7 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -76,7 +76,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 65e7df1e90..215c77a1bd 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -23,7 +23,6 @@ import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; import { Device, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models'; import { MatStepper, StepperOrientation } from '@angular/material/stepper'; -import { BaseData, HasId } from '@shared/models/base-data'; import { EntityType } from '@shared/models/entity-type.models'; import { Observable, throwError } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; @@ -40,7 +39,7 @@ import { HttpErrorResponse } from '@angular/common/http'; templateUrl: './device-wizard-dialog.component.html', styleUrls: ['./device-wizard-dialog.component.scss'] }) -export class DeviceWizardDialogComponent extends DialogComponent { +export class DeviceWizardDialogComponent extends DialogComponent { @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatStepper; @@ -64,7 +63,7 @@ export class DeviceWizardDialogComponent extends DialogComponent, protected router: Router, - public dialogRef: MatDialogRef, + public dialogRef: MatDialogRef, private deviceService: DeviceService, private breakpointObserver: BreakpointObserver, private fb: FormBuilder) { @@ -121,7 +120,7 @@ export class DeviceWizardDialogComponent extends DialogComponent this.dialogRef.close(true) + (device) => this.dialogRef.close(device) ); } } @@ -137,7 +136,7 @@ export class DeviceWizardDialogComponent extends DialogComponent> { + private createDevice(): Observable { const device: Device = { name: this.deviceWizardFormGroup.get('name').value, label: this.deviceWizardFormGroup.get('label').value, @@ -147,11 +146,8 @@ export class DeviceWizardDialogComponent extends DialogComponent 0) { return this.deviceService.saveDeviceWithCredentials(deepTrim(device), deepTrim(this.credentialsFormGroup.value.credential)).pipe( catchError((e: HttpErrorResponse) => { diff --git a/ui-ngx/src/app/modules/home/home.component.html b/ui-ngx/src/app/modules/home/home.component.html index 727ac490c3..697134759f 100644 --- a/ui-ngx/src/app/modules/home/home.component.html +++ b/ui-ngx/src/app/modules/home/home.component.html @@ -57,7 +57,7 @@
diff --git a/ui-ngx/src/app/modules/home/home.component.ts b/ui-ngx/src/app/modules/home/home.component.ts index 6e045b9786..f2985a93cc 100644 --- a/ui-ngx/src/app/modules/home/home.component.ts +++ b/ui-ngx/src/app/modules/home/home.component.ts @@ -14,10 +14,10 @@ /// limitations under the License. /// -import { AfterViewInit, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; -import { fromEvent } from 'rxjs'; +import { AfterViewInit, Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { startWith, skip, Subject } from 'rxjs'; import { Store } from '@ngrx/store'; -import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { PageComponent } from '@shared/components/page.component'; @@ -31,13 +31,14 @@ import { WINDOW } from '@core/services/window.service'; import { instanceOfSearchableComponent, ISearchableComponent } from '@home/models/searchable-component.models'; import { ActiveComponentService } from '@core/services/active-component.service'; import { RouterTabsComponent } from '@home/components/router-tabs.component'; +import { FormBuilder } from '@angular/forms'; @Component({ selector: 'tb-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'] }) -export class HomeComponent extends PageComponent implements AfterViewInit, OnInit { +export class HomeComponent extends PageComponent implements AfterViewInit, OnInit, OnDestroy { authState: AuthState = getCurrentAuthState(this.store); @@ -60,14 +61,17 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni searchEnabled = false; showSearch = false; - searchText = ''; + textSearch = this.fb.control('', {nonNullable: true}); hideLoadingBar = false; + private destroy$ = new Subject(); + constructor(protected store: Store, @Inject(WINDOW) private window: Window, private activeComponentService: ActiveComponentService, - public breakpointObserver: BreakpointObserver) { + public breakpointObserver: BreakpointObserver, + private fb: FormBuilder) { super(store); } @@ -79,6 +83,7 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni this.breakpointObserver .observe(MediaBreakpoints['gt-sm']) + .pipe(takeUntil(this.destroy$)) .subscribe((state: BreakpointState) => { if (state.matches) { this.sidenavMode = 'side'; @@ -91,16 +96,19 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni ); } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + ngAfterViewInit() { - fromEvent(this.searchInputField.nativeElement, 'keyup') - .pipe( - debounceTime(150), - distinctUntilChanged(), - tap(() => { - this.searchTextUpdated(); - }) - ) - .subscribe(); + this.textSearch.valueChanges.pipe( + debounceTime(150), + startWith(''), + distinctUntilChanged((a: string, b: string) => a.trim() === b.trim()), + skip(1), + takeUntil(this.destroy$) + ).subscribe(value => this.searchTextUpdated(value.trim())); } sidenavClicked() { @@ -136,7 +144,7 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni private updateActiveComponent(activeComponent: any) { this.showSearch = false; - this.searchText = ''; + this.textSearch.reset('', {emitEvent: false}); this.activeComponent = activeComponent; this.hideLoadingBar = activeComponent && activeComponent instanceof RouterTabsComponent; if (this.activeComponent && instanceOfSearchableComponent(this.activeComponent)) { @@ -165,16 +173,15 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni closeSearch() { if (this.searchEnabled) { this.showSearch = false; - if (this.searchText.length) { - this.searchText = ''; - this.searchTextUpdated(); + if (this.textSearch.value.length) { + this.textSearch.reset(); } } } - private searchTextUpdated() { + private searchTextUpdated(searchText: string) { if (this.searchableComponent) { - this.searchableComponent.onSearchTextUpdated(this.searchText); + this.searchableComponent.onSearchTextUpdated(searchText); } } } diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.html b/ui-ngx/src/app/modules/home/menu/menu-link.component.html index ab5b1faa27..0efc34a31c 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.html @@ -16,7 +16,6 @@ --> - {{section.icon}} - + {{section.icon}} {{section.name | translate}} diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html index de4f16b163..448c67a513 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html @@ -16,8 +16,7 @@ --> - {{section.icon}} - + {{section.icon}} {{section.name | translate}} diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.scss b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss index fc9df865a1..dbba5e78a9 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.scss +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss @@ -49,7 +49,7 @@ &.tb-active { background-color: rgba(255, 255, 255, .15); } - mat-icon { + .mat-icon { margin-right: 8px; margin-left: 0; min-width: 1.125rem; 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 e9787260bb..8104e80e36 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 @@ -33,6 +33,7 @@ import { IAliasController, IStateController } from '@app/core/api/widget-api.mod import { enumerable } from '@shared/decorators/enumerable'; import { UtilsService } from '@core/services/utils.service'; import { TbPopoverComponent } from '@shared/components/popover.component'; +import { ComponentStyle, iconStyle, textStyle } from '@shared/models/widget-settings.models'; export interface WidgetsData { widgets: Array; @@ -330,15 +331,14 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { margin: string; borderRadius: string; - title: string; - customTranslatedTitle: string; + title$: Observable; titleTooltip: string; showTitle: boolean; - titleStyle: {[klass: string]: any}; + titleStyle: ComponentStyle; titleIcon: string; showTitleIcon: boolean; - titleIconStyle: {[klass: string]: any}; + titleIconStyle: ComponentStyle; dropShadow: boolean; enableFullscreen: boolean; @@ -351,7 +351,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { onlyHistoryTimewindow: boolean; - style: {[klass: string]: any}; + style: ComponentStyle; showWidgetTitlePanel: boolean; showWidgetActions: boolean; @@ -430,25 +430,23 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { this.margin = this.widget.config.margin || '0px'; this.borderRadius = this.widget.config.borderRadius; - this.title = isDefined(this.widgetContext.widgetTitle) + const title = isDefined(this.widgetContext.widgetTitle) && this.widgetContext.widgetTitle.length ? this.widgetContext.widgetTitle : this.widget.config.title; - this.customTranslatedTitle = this.dashboard.utils.customTranslation(this.title, this.title); + this.title$ = this.widgetContext.registerLabelPattern(title, this.title$); this.titleTooltip = isDefined(this.widgetContext.widgetTitleTooltip) && this.widgetContext.widgetTitleTooltip.length ? this.widgetContext.widgetTitleTooltip : this.widget.config.titleTooltip; this.titleTooltip = this.dashboard.utils.customTranslation(this.titleTooltip, this.titleTooltip); this.showTitle = isDefined(this.widget.config.showTitle) ? this.widget.config.showTitle : true; - this.titleStyle = this.widget.config.titleStyle ? this.widget.config.titleStyle : {}; - + this.titleStyle = {...(this.widget.config.titleStyle || {}), ...textStyle(this.widget.config.titleFont, 'normal')}; + if (this.widget.config.titleColor) { + this.titleStyle.color = this.widget.config.titleColor; + } this.titleIcon = isDefined(this.widget.config.titleIcon) ? this.widget.config.titleIcon : ''; this.showTitleIcon = isDefined(this.widget.config.showTitleIcon) ? this.widget.config.showTitleIcon : false; - this.titleIconStyle = {}; + this.titleIconStyle = this.widget.config.iconSize ? iconStyle(this.widget.config.iconSize) : {}; if (this.widget.config.iconColor) { this.titleIconStyle.color = this.widget.config.iconColor; } - if (this.widget.config.iconSize) { - this.titleIconStyle.fontSize = this.widget.config.iconSize; - } - this.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true; this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : true; @@ -491,14 +489,22 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { this.showWidgetActions = !this.widgetContext.hideTitlePanel; - this.updateCustomHeaderActions(); + this.updateParamsFromData(); this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : []; if (detectChanges) { this.widgetContext.detectContainerChanges(); } } - updateCustomHeaderActions(detectChanges = false) { + updateParamsFromData(detectChanges = false) { + this.widgetContext.updateLabelPatterns(); + const update = this.updateCustomHeaderActions(); + if (update && detectChanges) { + this.widgetContext.detectContainerChanges(); + } + } + + private updateCustomHeaderActions(): boolean { let customHeaderActions: Array; if (this.widgetContext.customHeaderActions) { let data: FormattedData[] = []; @@ -511,10 +517,9 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { } if (!isEqual(this.customHeaderActions, customHeaderActions)) { this.customHeaderActions = customHeaderActions; - if (detectChanges) { - this.widgetContext.detectContainerChanges(); - } + return true; } + return false; } private filterCustomHeaderAction(action: WidgetHeaderAction, data: FormattedData[]): boolean { diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index e9fb01c54a..bcd8e793b3 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -75,7 +75,6 @@ export interface GroupActionDescriptor> { export interface HeaderActionDescriptor { name: string; icon: string; - isMdiIcon?: boolean; isEnabled: () => boolean; onAction: ($event: MouseEvent) => void; } 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 c00d0a8e82..214a78d022 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 @@ -20,6 +20,7 @@ import { Datasource, DatasourceData, FormattedData, + fullWidgetTypeFqn, JsonSettingsSchema, Widget, WidgetActionDescriptor, @@ -30,6 +31,7 @@ import { widgetType, WidgetTypeDescriptor, WidgetTypeDetails, + widgetTypeFqn, WidgetTypeParameters } from '@shared/models/widget.models'; import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; @@ -45,13 +47,13 @@ import { WidgetActionsApi, WidgetSubscriptionApi } from '@core/api/widget-api.models'; -import { ChangeDetectorRef, ComponentFactory, Injector, NgZone, Type } from '@angular/core'; +import { ChangeDetectorRef, Injector, NgModuleRef, NgZone, Type } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { RafService } from '@core/services/raf.service'; import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { TenantId } from '@shared/models/id/tenant-id'; import { WidgetLayout } from '@shared/models/dashboard.models'; -import { formatValue, isDefined } from '@core/utils'; +import { createLabelFromDatasource, formatValue, hasDatasourceLabelsVariables, isDefined } from '@core/utils'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { @@ -84,13 +86,15 @@ import { DomSanitizer } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { EdgeService } from '@core/http/edge.service'; import * as RxJS from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import * as RxJSOperators from 'rxjs/operators'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { EntityId } from '@shared/models/id/entity-id'; -import { AlarmQuery, AlarmSearchStatus, AlarmStatus} from '@app/shared/models/alarm.models'; +import { AlarmQuery, AlarmSearchStatus, AlarmStatus } from '@app/shared/models/alarm.models'; import { MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api'; import { UserId } from '@shared/models/id/user-id'; import { UserSettingsService } from '@core/http/user-settings.service'; +import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service'; export interface IWidgetAction { name: string; @@ -200,6 +204,8 @@ export class WidgetContext { subscriptions: {[id: string]: IWidgetSubscription} = {}; defaultSubscription: IWidgetSubscription = null; + labelPatterns = new Map, LabelVariablePattern>(); + timewindowFunctions: TimewindowFunctions = { onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => { if (this.defaultSubscription) { @@ -243,6 +249,7 @@ export class WidgetContext { formatValue }; + $widgetElement: JQuery; $container: JQuery; $containerParent: JQuery; width: number; @@ -264,6 +271,8 @@ export class WidgetContext { hiddenData?: Array<{data: DataSet}>; timeWindow?: WidgetTimewindow; + embedTitlePanel?: boolean; + hideTitlePanel = false; widgetTitle?: string; @@ -309,6 +318,23 @@ export class WidgetContext { }); } + registerLabelPattern(label: string, label$: Observable): Observable { + let labelPattern = label$ ? this.labelPatterns.get(label$) : null; + if (labelPattern) { + labelPattern.setupPattern(label); + } else { + labelPattern = new LabelVariablePattern(label, this); + this.labelPatterns.set(labelPattern.label$, labelPattern); + } + return labelPattern.label$; + } + + updateLabelPatterns() { + for (const labelPattern of this.labelPatterns.values()) { + labelPattern.update(); + } + } + showSuccessToast(message: string, duration: number = 1000, verticalPosition: NotificationVerticalPosition = 'bottom', horizontalPosition: NotificationHorizontalPosition = 'left', @@ -403,6 +429,14 @@ export class WidgetContext { this.widgetActions = undefined; } + destroy() { + for (const labelPattern of this.labelPatterns.values()) { + labelPattern.destroy(); + } + this.labelPatterns.clear(); + this.destroyed = true; + } + closeDialog(resultData: any = null) { const dialogRef = this.$scope.dialogRef || this.stateController.dashboardCtrl.dashboardCtx.getDashboard().dialogRef; if (dialogRef) { @@ -414,15 +448,53 @@ export class WidgetContext { return new PageLink(pageSize, page, textSearch, sortOrder); } - timePageLink(startTime: number, endTime: number, pageSize: number, page: number = 0, textSearch: string = null, sortOrder: SortOrder = null) { + timePageLink(startTime: number, endTime: number, pageSize: number, page: number = 0, + textSearch: string = null, sortOrder: SortOrder = null) { return new TimePageLink(pageSize, page, textSearch, sortOrder, startTime, endTime); } - alarmQuery(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, status: AlarmStatus, fetchOriginator: boolean, assigneeId: UserId) { + alarmQuery(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, + status: AlarmStatus, fetchOriginator: boolean, assigneeId: UserId) { return new AlarmQuery(entityId, pageLink, searchStatus, status, fetchOriginator, assigneeId); } } +export class LabelVariablePattern { + + private pattern: string; + private hasVariables: boolean; + + private labelSubject = new BehaviorSubject(''); + + public label$ = this.labelSubject.asObservable(); + + constructor(label: string, + private ctx: WidgetContext) { + this.setupPattern(label); + } + + setupPattern(label: string) { + this.pattern = this.ctx.dashboard.utils.customTranslation(label, label); + this.hasVariables = hasDatasourceLabelsVariables(this.pattern); + this.update(); + } + + update() { + let label = this.pattern; + const datasource = this.ctx.defaultSubscription?.firstDatasource; + if (this.hasVariables && datasource) { + label = createLabelFromDatasource(datasource, label); + } + if (this.labelSubject.value !== label) { + this.labelSubject.next(label); + } + } + + destroy() { + this.labelSubject.complete(); + } +} + export interface IDynamicWidgetComponent { readonly ctx: WidgetContext; readonly errorMessages: string[]; @@ -437,13 +509,15 @@ export interface IDynamicWidgetComponent { export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { widgetName: string; - alias: string; + fullFqn: string; + deprecated: boolean; typeSettingsSchema?: string | any; typeDataKeySettingsSchema?: string | any; typeLatestDataKeySettingsSchema?: string | any; image?: string; description?: string; - componentFactory?: ComponentFactory; + componentType?: Type; + componentModuleRef?: NgModuleRef; } export interface WidgetConfigComponentData { @@ -460,13 +534,15 @@ export interface WidgetConfigComponentData { settingsDirective: string; dataKeySettingsDirective: string; latestDataKeySettingsDirective: string; + hasBasicMode: boolean; basicModeDirective: string; } export const MissingWidgetType: WidgetInfo = { type: widgetType.latest, widgetName: 'Widget type not found', - alias: 'undefined', + fullFqn: 'undefined', + deprecated: false, sizeX: 8, sizeY: 6, resources: [], @@ -490,7 +566,8 @@ export const MissingWidgetType: WidgetInfo = { export const ErrorWidgetType: WidgetInfo = { type: widgetType.latest, widgetName: 'Error loading widget', - alias: 'error', + fullFqn: 'error', + deprecated: false, sizeX: 8, sizeY: 6, resources: [], @@ -529,48 +606,37 @@ export interface WidgetTypeInstance { onDestroy?: () => void; } -export function detailsToWidgetInfo(widgetTypeDetailsEntity: WidgetTypeDetails): WidgetInfo { +export const toWidgetInfo = (widgetTypeEntity: WidgetType): WidgetInfo => ({ + widgetName: widgetTypeEntity.name, + fullFqn: fullWidgetTypeFqn(widgetTypeEntity), + deprecated: widgetTypeEntity.deprecated, + type: widgetTypeEntity.descriptor.type, + sizeX: widgetTypeEntity.descriptor.sizeX, + sizeY: widgetTypeEntity.descriptor.sizeY, + resources: widgetTypeEntity.descriptor.resources, + templateHtml: widgetTypeEntity.descriptor.templateHtml, + templateCss: widgetTypeEntity.descriptor.templateCss, + controllerScript: widgetTypeEntity.descriptor.controllerScript, + settingsSchema: widgetTypeEntity.descriptor.settingsSchema, + dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema, + latestDataKeySettingsSchema: widgetTypeEntity.descriptor.latestDataKeySettingsSchema, + settingsDirective: widgetTypeEntity.descriptor.settingsDirective, + dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective, + latestDataKeySettingsDirective: widgetTypeEntity.descriptor.latestDataKeySettingsDirective, + hasBasicMode: widgetTypeEntity.descriptor.hasBasicMode, + basicModeDirective: widgetTypeEntity.descriptor.basicModeDirective, + defaultConfig: widgetTypeEntity.descriptor.defaultConfig +}); + +export const detailsToWidgetInfo = (widgetTypeDetailsEntity: WidgetTypeDetails): WidgetInfo => { const widgetInfo = toWidgetInfo(widgetTypeDetailsEntity); widgetInfo.image = widgetTypeDetailsEntity.image; widgetInfo.description = widgetTypeDetailsEntity.description; return widgetInfo; -} - -export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { - return { - widgetName: widgetTypeEntity.name, - alias: widgetTypeEntity.alias, - type: widgetTypeEntity.descriptor.type, - sizeX: widgetTypeEntity.descriptor.sizeX, - sizeY: widgetTypeEntity.descriptor.sizeY, - resources: widgetTypeEntity.descriptor.resources, - templateHtml: widgetTypeEntity.descriptor.templateHtml, - templateCss: widgetTypeEntity.descriptor.templateCss, - controllerScript: widgetTypeEntity.descriptor.controllerScript, - settingsSchema: widgetTypeEntity.descriptor.settingsSchema, - dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema, - latestDataKeySettingsSchema: widgetTypeEntity.descriptor.latestDataKeySettingsSchema, - settingsDirective: widgetTypeEntity.descriptor.settingsDirective, - dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective, - latestDataKeySettingsDirective: widgetTypeEntity.descriptor.latestDataKeySettingsDirective, - hasBasicMode: widgetTypeEntity.descriptor.hasBasicMode, - basicModeDirective: widgetTypeEntity.descriptor.basicModeDirective, - defaultConfig: widgetTypeEntity.descriptor.defaultConfig - }; -} - -export function toWidgetTypeDetails(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, - bundleAlias: string, createdTime: number): WidgetTypeDetails { - const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, bundleAlias, createdTime); - const widgetTypeDetails: WidgetTypeDetails = {...widgetTypeEntity, - description: widgetInfo.description, - image: widgetInfo.image - }; - return widgetTypeDetails; -} +}; -export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, - bundleAlias: string, createdTime: number): WidgetType { +export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, + createdTime: number): WidgetType => { const descriptor: WidgetTypeDescriptor = { type: widgetInfo.type, sizeX: widgetInfo.sizeX, @@ -593,15 +659,25 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: id, tenantId, createdTime, - bundleAlias, - alias: widgetInfo.alias, + fqn: widgetTypeFqn(widgetInfo.fullFqn), name: widgetInfo.widgetName, + deprecated: widgetInfo.deprecated, descriptor }; -} +}; + +export const toWidgetTypeDetails = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, + createdTime: number): WidgetTypeDetails => { + const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, createdTime); + return { + ...widgetTypeEntity, + description: widgetInfo.description, + image: widgetInfo.image + }; +}; -export function updateEntityParams(params: StateParams, targetEntityParamName?: string, targetEntityId?: EntityId, - entityName?: string, entityLabel?: string) { +export const updateEntityParams = (params: StateParams, targetEntityParamName?: string, targetEntityId?: EntityId, + entityName?: string, entityLabel?: string) => { if (targetEntityId) { let targetEntityParams: StateParams; if (targetEntityParamName && targetEntityParamName.length) { @@ -622,4 +698,4 @@ export function updateEntityParams(params: StateParams, targetEntityParamName?: targetEntityParams.entityLabel = entityLabel; } } -} +}; diff --git a/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts b/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts new file mode 100644 index 0000000000..e2c997f654 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts @@ -0,0 +1,65 @@ +/// +/// Copyright © 2016-2023 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 { inject, NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { RouterTabsComponent } from '@home/components/router-tabs.component'; +import { Authority } from '@shared/models/authority.enum'; +import { securityRoutes } from '@home/pages/security/security-routing.module'; +import { profileRoutes } from '@home/pages/profile/profile-routing.module'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + notificationUserSettingsRoutes +} from '@home/pages/notification/settings/notification-settings-routing.modules'; + +const routes: Routes = [ + { + path: 'account', + component: RouterTabsComponent, + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + breadcrumb: { + label: 'account.account', + icon: 'account_circle' + }, + useChildrenRoutesForTabs: true, + }, + resolve: { + replaceUrl: () => getCurrentAuthState(inject(Store)).forceFullscreen + }, + children: [ + { + path: '', + children: [], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + redirectTo: '/account/profile', + } + }, + ...profileRoutes, + ...securityRoutes, + ...notificationUserSettingsRoutes + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class AccountRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/account/account.module.ts b/ui-ngx/src/app/modules/home/pages/account/account.module.ts new file mode 100644 index 0000000000..df178607ce --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/account/account.module.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2023 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 { AccountRoutingModule } from '@home/pages/account/account-routing.module'; +import { CommonModule } from '@angular/common'; + +@NgModule({ + declarations: [ ], + imports: [ + CommonModule, + AccountRoutingModule + ] +}) +export class AccountModule { } 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 adc87a7886..5d2727533c 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 @@ -36,7 +36,7 @@ import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table- import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component'; import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component'; import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; -import { widgetsBundlesRoutes } from '@home/pages/widget/widget-library-routing.module'; +import { widgetsLibraryRoutes } from '@home/pages/widget/widget-library-routing.module'; import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module'; @@ -67,10 +67,10 @@ const routes: Routes = [ children: [], data: { auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], - redirectTo: '/resources/widgets-bundles' + redirectTo: '/resources/widgets-library' } }, - ...widgetsBundlesRoutes, + ...widgetsLibraryRoutes, { path: 'resources-library', data: { @@ -319,8 +319,7 @@ const routes: Routes = [ title: 'admin.2fa.2fa', breadcrumb: { label: 'admin.2fa.2fa', - icon: 'mdi:two-factor-authentication', - isMdiIcon: true + icon: 'mdi:two-factor-authentication' } } }, diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss index cd6c18af9e..e8da0e042a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import "../../../../../theme"; +@import "../../../../../scss/constants"; :host { .fields-group { diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html index 16229a7af6..b2c4b5a10a 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html @@ -86,13 +86,6 @@ {{ 'asset.name-max-length' | translate }}
- - asset.label @@ -100,6 +93,20 @@ {{ 'asset.label-max-length' | translate }} + + + +
asset.description diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts b/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts index 8c53cbba38..a96b65e249 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts @@ -69,6 +69,7 @@ export class AssetComponent extends EntityComponent { name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], assetProfileId: [entity ? entity.assetProfileId : null, [Validators.required]], label: [entity ? entity.label : '', Validators.maxLength(255)], + customerId: [entity ? entity.customerId : ''], additionalInfo: this.fb.group( { description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], @@ -82,6 +83,7 @@ export class AssetComponent extends EntityComponent { this.entityForm.patchValue({name: entity.name}); this.entityForm.patchValue({assetProfileId: entity.assetProfileId}); this.entityForm.patchValue({label: entity.label}); + this.entityForm.patchValue({customerId: entity.customerId}); this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts index 097902d087..084faebefc 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts @@ -103,9 +103,7 @@ export class DashboardsTableConfigResolver implements Resolve this.translate.instant('dashboard.delete-dashboards-text'); this.config.loadEntity = id => this.dashboardService.getDashboard(id.id); - this.config.saveEntity = dashboard => { - return this.dashboardService.saveDashboard(dashboard as Dashboard); - }; + this.config.saveEntity = dashboard => this.dashboardService.saveDashboard(dashboard as Dashboard); this.config.onEntityAction = action => this.onDashboardAction(action); this.config.detailsReadonly = () => (this.config.componentsData.dashboardScope === 'customer_user' || this.config.componentsData.dashboardScope === 'edge_customer_user'); @@ -118,6 +116,10 @@ export class DashboardsTableConfigResolver implements Resolve { + this.openDashboard(null, dashboard); + }; } resolve(route: ActivatedRouteSnapshot): Observable> { @@ -178,13 +180,9 @@ export class DashboardsTableConfigResolver implements Resolve('customersTitle', 'dashboard.assignedToCustomers', - '50%', entity => { - return getDashboardAssignedCustomersText(entity); - }, () => ({}), false), + '50%', entity => getDashboardAssignedCustomersText(entity), () => ({}), false), new EntityTableColumn('dashboardIsPublic', 'dashboard.public', '60px', - entity => { - return checkBoxCell(isPublicDashboard(entity)); - }, () => ({}), false), + entity => checkBoxCell(isPublicDashboard(entity)), () => ({}), false), ); } return columns; @@ -269,7 +267,7 @@ export class DashboardsTableConfigResolver implements Resolve true, + isEnabled: () => true, onAction: ($event, entity) => this.unassignFromEdge($event, entity) } ); @@ -383,7 +381,7 @@ export class DashboardsTableConfigResolver implements Resolve { if (dashboard) { diff --git a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html new file mode 100644 index 0000000000..9b0b18e2c2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html @@ -0,0 +1,401 @@ + + +

{{ dialogTitle }}

+ + + +
+
+
+ + + {{ deviceTransportTypeTranslationMap.get(BasicTransportType.HTTP) | translate }} + + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.MQTT) | translate }} + + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.COAP) | translate }} + + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.SNMP) | translate }} + + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.LWM2M) | translate }} + + +
+ + +
device.connectivity.use-following-instructions
+ + + + + Windows + + +
+
+
device.connectivity.install-necessary-client-tools
+
device.connectivity.install-curl-windows
+
+ + +
+
+
+ + + + MacOS + + +
+
+
device.connectivity.install-necessary-client-tools
+
device.connectivity.install-curl-macos
+
+ + +
+
+
+ + + + Linux + + +
+
+
device.connectivity.install-necessary-client-tools
+ +
+ + +
+
+
+
+
+ +
+ + +
+ +
device.connectivity.use-following-instructions
+ + + + + Windows + + +
+
+
device.connectivity.install-necessary-client-tools
+
+ + +
+
+ + +
+
+
+ + + + MacOS + + +
+
+
device.connectivity.install-necessary-client-tools
+ +
+ + +
+
+
+ + + + Linux + + +
+
+
device.connectivity.install-necessary-client-tools
+ +
+ + +
+
+
+ + + + Docker + + +
+ + +
+
+
+
+
+
+ +
device.connectivity.use-following-instructions
+ + + + + MacOS + + +
+
+
device.connectivity.install-necessary-client-tools
+
+ + +
+
+ + +
+
+
+ + + + Linux + + +
+
+
device.connectivity.install-necessary-client-tools
+
+ + +
+
+ + +
+
+
+ + + + Docker + + +
+ + +
+
+
+
+
+ +
+ + +
+
+ +
+ + +
+
+
+
+
+
+
device.state
+
+ {{ (status ? 'device.active' : 'device.inactive') | translate }} +
+
+
attribute.latest-telemetry
+
+
+
device.time
+
attribute.key
+
attribute.value
+
+
+
+
{{ telemetry.lastUpdateTs | date: 'yyyy-MM-dd HH:mm:ss' }}
+
{{ telemetry.key }}
+
{{ telemetry.value }}
+
+
+
+
+
+
+
+ {{ 'action.dont-show-again' | translate}} + + +
+ +
+ + + {{ 'device.connectivity.loading-check-connectivity-command' | translate }} + +
+
+ +
+
+
attribute.no-latest-telemetry
+
+
+ + +
+
+
device.connectivity.execute-following-command
+ + {{ cmd.noSecLabel }} + {{ cmd.secLabel }} + +
+ + + + + +
+ +
+ + + + +
+
+
+
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss new file mode 100644 index 0000000000..348fa01a26 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss @@ -0,0 +1,177 @@ +/** + * Copyright © 2016-2023 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"; + +:host { + height: 100%; + max-height: 100vh; + display: grid; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + + .tb-loader { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + height: 300px; + max-height: 100%; + + .label { + margin-bottom: 0; + text-align: center; + } + } + + .status { + margin-left: 12px; + border-radius: 12px; + height: 24px; + line-height: 24px; + padding: 0 8px; + width: fit-content; + color: #198038; + background-color: rgba(25, 128, 56, 0.08); + font-size: 14px; + + &.inactive { + color: #d12730; + background-color: rgba(209, 39, 48, 0.08); + } + } + + .tb-hint-instruction { + border-radius: 6px; + background-color: rgba(48, 86, 128, 0.04); + padding: 6px 16px; + + .content { + vertical-align: middle; + } + } + + .tb-font-14 { + font-size: 14px; + } + + .tb-flex-1 { + flex: 1; + } + + .tb-form-table-body { + max-height: 108px; + min-height: 108px; + overflow-y: auto; + scrollbar-gutter: stable; + + .tb-form-table-row { + min-height: 38px; + } + } + + .tb-no-data-available { + .tb-no-data-bg { + min-height: 68px; + } + } + + .tb-install-instruction-text { + min-height: 42px; + } + + @media #{$mat-sm} { + width: 470px; + } + + @media #{$mat-gt-sm} { + width: 720px; + } +} + +:host-context(.mat-mdc-dialog-container) { + .tb-dialog-actions { + display: flex; + gap: 8px; + padding: 8px 16px; + } + + .mat-mdc-dialog-content { + max-height: 80vh; + padding: 16px; + } +} + +:host ::ng-deep { + .tb-markdown-view { + .tb-command-code { + .code-wrapper { + padding: 0; + pre[class*=language-] { + margin: 0; + background: #F3F6FA; + border-color: #305680; + padding-right: 38px; + overflow: scroll; + padding-bottom: 4px; + + &::-webkit-scrollbar { + width: 4px; + height: 4px; + } + } + } + button.clipboard-btn { + right: -2px; + p { + color: #305680; + } + p, div { + background-color: #F3F6FA; + } + div { + img { + display: none; + } + &:after { + content: ""; + position: initial; + display: block; + width: 18px; + height: 18px; + background: #305680; + mask-image: url(/assets/copy-code-icon.svg); + mask-repeat: no-repeat; + } + } + } + } + } + .mdc-button__label > span { + .mat-icon { + vertical-align: text-bottom; + box-sizing: initial; + } + } + + .tabs-icon { + margin-right: 8px; + } + + .tb-form-panel.tb-tab-body { + padding: 16px 0 0; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts new file mode 100644 index 0000000000..eeb54381df --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts @@ -0,0 +1,219 @@ +/// +/// Copyright © 2016-2023 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, Inject, NgZone, OnDestroy, OnInit } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { DeviceService } from '@core/http/device.service'; +import { + AttributeData, + AttributeScope, + AttributesSubscriptionCmd, + LatestTelemetry, + TelemetrySubscriber +} from '@shared/models/telemetry/telemetry.models'; +import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { selectPersistDeviceStateToTelemetry } from '@core/auth/auth.selectors'; +import { take } from 'rxjs/operators'; +import { + BasicTransportType, + DeviceTransportType, + deviceTransportTypeTranslationMap, + NetworkTransportType, + PublishTelemetryCommand +} from '@shared/models/device.models'; +import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { getOS } from '@core/utils'; + +export interface DeviceCheckConnectivityDialogData { + deviceId: EntityId; + afterAdd: boolean; +} + +@Component({ + selector: 'tb-device-check-connectivity-dialog', + templateUrl: './device-check-connectivity-dialog.component.html', + styleUrls: ['./device-check-connectivity-dialog.component.scss'] +}) +export class DeviceCheckConnectivityDialogComponent extends + DialogComponent implements OnInit, OnDestroy { + + loadedCommand = false; + + status: boolean; + + latestTelemetry: Array = []; + + commands: PublishTelemetryCommand; + + allowTransportType = new Set(); + selectTransportType: NetworkTransportType; + + BasicTransportType = BasicTransportType; + DeviceTransportType = DeviceTransportType; + deviceTransportTypeTranslationMap = deviceTransportTypeTranslationMap; + + showDontShowAgain: boolean; + dialogTitle: string; + + notShowAgain = false; + + httpTabIndex = 0; + mqttTabIndex = 0; + coapTabIndex = 0; + + private telemetrySubscriber: TelemetrySubscriber; + + private currentTime = Date.now(); + + private transportTypes = [...Object.keys(BasicTransportType), ...Object.keys(DeviceTransportType)] as Array; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) private data: DeviceCheckConnectivityDialogData, + public dialogRef: MatDialogRef, + private deviceService: DeviceService, + private telemetryWsService: TelemetryWebsocketService, + private zone: NgZone) { + super(store, router, dialogRef); + + if (this.data.afterAdd) { + this.dialogTitle = 'device.connectivity.device-created-check-connectivity'; + this.showDontShowAgain = true; + } else { + this.dialogTitle = 'device.connectivity.check-connectivity'; + this.showDontShowAgain = false; + } + } + + ngOnInit() { + this.loadCommands(); + this.subscribeToLatestTelemetry(); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.telemetrySubscriber?.complete(); + this.telemetrySubscriber?.unsubscribe(); + } + + close(): void { + if (this.notShowAgain && this.showDontShowAgain) { + this.store.dispatch(new ActionPreferencesPutUserSettings({ notDisplayConnectivityAfterAddDevice: true })); + this.dialogRef.close(null); + } else { + this.dialogRef.close(null); + } + } + + createMarkDownCommand(commands: string | string[]): string { + if (Array.isArray(commands)) { + const formatCommands: Array = []; + commands.forEach(command => formatCommands.push(this.createMarkDownSingleCommand(command))); + return formatCommands.join(`\n
\n\n`); + } else { + return this.createMarkDownSingleCommand(commands); + } + } + + private createMarkDownSingleCommand(command: string): string { + return '```bash\n' + + command + + '{:copy-code}\n' + + '```'; + } + + private loadCommands() { + this.deviceService.getDevicePublishTelemetryCommands(this.data.deviceId.id).subscribe( + commands => { + this.commands = commands; + const commandsProtocols = Object.keys(commands); + this.transportTypes.forEach(transport => { + const findCommand = commandsProtocols.find(item => item.toUpperCase().startsWith(transport)); + if (findCommand) { + this.allowTransportType.add(transport); + } + }); + this.selectTransportType = this.allowTransportType.values().next().value; + this.selectTabIndexForUserOS(); + this.loadedCommand = true; + } + ); + } + + private subscribeToLatestTelemetry() { + this.store.pipe(select(selectPersistDeviceStateToTelemetry)).pipe( + take(1) + ).subscribe(persistToTelemetry => { + this.telemetrySubscriber = TelemetrySubscriber.createEntityAttributesSubscription( + this.telemetryWsService, this.data.deviceId, LatestTelemetry.LATEST_TELEMETRY, this.zone); + if (!persistToTelemetry) { + const subscriptionCommand = new AttributesSubscriptionCmd(); + subscriptionCommand.entityType = this.data.deviceId.entityType as EntityType; + subscriptionCommand.entityId = this.data.deviceId.id; + subscriptionCommand.scope = AttributeScope.SERVER_SCOPE; + subscriptionCommand.keys = 'active'; + this.telemetrySubscriber.subscriptionCommands.push(subscriptionCommand); + } + + this.telemetrySubscriber.subscribe(); + this.telemetrySubscriber.attributeData$().subscribe( + (data) => { + const telemetry = data.reduce>((accumulator, item) => { + if (item.key === 'active') { + this.status = coerceBooleanProperty(item.value); + } else if (item.lastUpdateTs > this.currentTime) { + accumulator.push(item); + } + return accumulator; + }, []); + this.latestTelemetry = telemetry.sort((a, b) => b.lastUpdateTs - a.lastUpdateTs); + } + ); + }); + } + + private selectTabIndexForUserOS() { + const currentOS = getOS(); + switch (currentOS) { + case 'linux': + case 'android': + this.httpTabIndex = 2; + this.mqttTabIndex = 2; + this.coapTabIndex = 1; + break; + case 'macos': + case 'ios': + this.httpTabIndex = 1; + this.mqttTabIndex = 1; + break; + case 'windows': + this.httpTabIndex = 0; + this.mqttTabIndex = 0; + break; + default: + this.mqttTabIndex = this.commands.mqtt?.docker ? 3 : 0; + this.coapTabIndex = this.commands.coap?.docker ? 2 : 1; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html index 244b1c000b..0b1ef5225c 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -46,6 +46,12 @@ [fxShow]="!isEdit"> {{ ((deviceScope === 'customer_user' || deviceScope === 'edge_customer_user') ? 'device.view-credentials' : 'device.manage-credentials') | translate }} + diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss index 0702d6bf52..a44c99a45c 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss @@ -14,15 +14,34 @@ * limitations under the License. */ @import "../../../../../../scss/constants"; -@import "../../../../../../theme"; -:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { +:host { width: 820px; height: 100%; max-width: 100%; max-height: 100vh; - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: min-content 4px minmax(auto, 1fr) min-content min-content; +} + +:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { + .mat-mdc-dialog-content { + grid-row: 3; + display: flex; + flex-direction: column; + height: 100%; + padding: 0; + color: rgba(0, 0, 0, 0.87); + } + + .tb-dialog-actions { + grid-row: 5; + display: flex; + } + + .mat-divider { + grid-row: 4; + } .tb-title { font-size: 16px; @@ -76,6 +95,7 @@ .delivery-method-container { display: inline-flex; flex: 1 1 calc(50% - 8px); + max-width: calc(50% - 8px); padding: 16px 12px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 6px; @@ -178,17 +198,28 @@ line-height: 20px; overflow-x: auto; } + + &.mini { + font-size: 12px; + line-height: 1.25; + + .subject { + font-size: 14px; + line-height: 1.5; + padding-bottom: 4px; + } + } } } + + .tb-notification-use-template-toggle-group { + margin-bottom: 24px; + width: 320px; + } } :host ::ng-deep { .mat-mdc-dialog-content { - display: flex; - flex-direction: column; - height: 100%; - padding: 0 !important; - .mat-stepper-horizontal { display: flex; height: 100%; @@ -209,60 +240,14 @@ } } } - } - .mat-button-toggle-group.tb-notification-use-template-toggle-group { - &.mat-button-toggle-group-appearance-standard { - border: none; - border-radius: 18px; - margin-bottom: 24px; + .tb-form-panel .mat-expansion-panel.tb-settings { + padding: 11px 16px; - .mat-button-toggle + .mat-button-toggle { - border-left: none; + & > .mat-expansion-panel-content > .mat-expansion-panel-body { + gap: 0; } } - - .mat-button-toggle { - background: rgba(0, 0, 0, 0.06); - height: 36px; - align-items: center; - display: flex; - - .mat-button-toggle-ripple { - top: 2px; - left: 2px; - right: 2px; - bottom: 2px; - border-radius: 18px; - } - } - - .mat-button-toggle-button { - color: #959595; - } - - .mat-button-toggle-focus-overlay { - border-radius: 18px; - margin: 2px; - } - - .mat-button-toggle-checked .mat-button-toggle-button { - background-color: $tb-primary-color; - color: #fff; - border-radius: 18px; - margin-left: 2px; - margin-right: 2px; - } - - .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { - line-height: 34px; - font-size: 16px; - font-weight: 500; - } - - .mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { - opacity: .01; - } } .preview-group { @@ -273,9 +258,7 @@ } } } -} -:host ::ng-deep { .delivery-methods-container { .delivery-method-container { &.interact * { diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html new file mode 100644 index 0000000000..284cd67fa1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html @@ -0,0 +1,40 @@ + +
+
+
+ + + {{notificationTemplateTypeTranslateMap.get(notificationSettingsFormGroup.get('name').value)?.name | translate}} + + +
+
+
+ +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.scss b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.scss similarity index 72% rename from ui-ngx/src/app/modules/home/pages/widget/widget-library.component.scss rename to ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.scss index 8fcd595570..ee37181a09 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.scss @@ -14,19 +14,11 @@ * limitations under the License. */ :host { - button.tb-add-new-widget { - height: auto; - padding-right: 12px; - font-size: 24px; - border-style: dashed; - border-width: 2px; - } -} + .notification-type { + font-size: 14px; -:host ::ng-deep { - .tb-widget-library { - .tb-widget-container { - cursor: pointer; + &-disabled { + color: rgba(0, 0, 0, 0.38) } } } diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts new file mode 100644 index 0000000000..76e9babf00 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts @@ -0,0 +1,126 @@ +/// +/// Copyright © 2016-2023 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, OnDestroy, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { UtilsService } from '@core/services/utils.service'; +import { isDefinedAndNotNull } from '@core/utils'; +import { Subscription } from 'rxjs'; +import { + NotificationDeliveryMethod, + NotificationTemplateTypeTranslateMap, + NotificationUserSetting +} from '@shared/models/notification.models'; + +@Component({ + selector: 'tb-notification-setting-form', + templateUrl: './notification-setting-form.component.html', + styleUrls: ['./notification-setting-form.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NotificationSettingFormComponent), + multi: true + } + ] +}) +export class NotificationSettingFormComponent implements ControlValueAccessor, OnInit, OnDestroy { + + @Input() + disabled: boolean; + + @Input() + deliveryMethods: NotificationDeliveryMethod[] = []; + + @Input() + allowDeliveryMethods: NotificationDeliveryMethod[] = []; + + notificationSettingsFormGroup: UntypedFormGroup; + + notificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap; + + private propagateChange = null; + + private valueChange$: Subscription = null; + + constructor(private utils: UtilsService, + private fb: UntypedFormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + const deliveryMethod = {}; + this.deliveryMethods.forEach(value => { + deliveryMethod[value] = true; + }); + this.notificationSettingsFormGroup = this.fb.group( + { + name: [''], + enabled: [true], + enabledDeliveryMethods: this.fb.group({ + ...deliveryMethod + }) + }); + this.valueChange$ = this.notificationSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + ngOnDestroy() { + if (this.valueChange$) { + this.valueChange$.unsubscribe(); + this.valueChange$ = null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.notificationSettingsFormGroup.disable({emitEvent: false}); + } else { + this.notificationSettingsFormGroup.enable({emitEvent: false}); + } + } + + toggleEnabled() { + this.notificationSettingsFormGroup.get('enabled').patchValue(!this.notificationSettingsFormGroup.get('enabled').value); + } + + getChecked(deliveryMethod: NotificationDeliveryMethod): boolean { + return this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod).value; + } + + toggleDeliviryMethod(deliveryMethod: NotificationDeliveryMethod) { + this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod) + .patchValue(!this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod).value); + } + + writeValue(value: NotificationUserSetting): void { + if (isDefinedAndNotNull(value)) { + this.notificationSettingsFormGroup.patchValue(value, {emitEvent: false}); + } + } + + private updateModel() { + this.propagateChange(this.notificationSettingsFormGroup.value); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings-routing.modules.ts b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings-routing.modules.ts new file mode 100644 index 0000000000..6a65e554bf --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings-routing.modules.ts @@ -0,0 +1,41 @@ +/// +/// Copyright © 2016-2023 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 { Routes } from '@angular/router'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { Authority } from '@shared/models/authority.enum'; +import { inject, NgModule } from '@angular/core'; +import { NotificationSettingsComponent } from '@home/pages/notification/settings/notification-settings.component'; +import { NotificationService } from '@core/http/notification.service'; + +export const notificationUserSettingsRoutes: Routes = [ + { + path: 'notificationSettings', + component: NotificationSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'account.notification-settings', + breadcrumb: { + label: 'account.notification-settings', + icon: 'settings' + } + }, + resolve: { + userSettings: () => inject(NotificationService).getNotificationUserSettings() + } + } +]; diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html new file mode 100644 index 0000000000..c531f10d78 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html @@ -0,0 +1,84 @@ + +
+ + +
+
+ notification.settings.notification-settings +
+
+ +
+
+
+ + +
+ +
+
+
+
+
+ + notification.settings.type + +
+
+
+ + {{ notificationDeliveryMethodTranslateMap.get(deliveryMethods) | translate }} + +
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.scss b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.scss new file mode 100644 index 0000000000..a91c57ed65 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.scss @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2023 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"; + +:host { + .mat-mdc-card.settings-card { + margin: 8px; + @media #{$mat-gt-sm} { + width: 60%; + } + .mat-headline-5 { + margin: 0; + } + .notification-form { + height: 100%; + min-height: min-content; + max-height: min-content; + } + .notification-section { + height: 100%; + border: 1px solid rgba(0, 0, 0, 0.12); + overflow: scroll; + &-block { + min-width: 470px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts new file mode 100644 index 0000000000..aada799885 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts @@ -0,0 +1,189 @@ +/// +/// Copyright © 2016-2023 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, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { TranslateService } from '@ngx-translate/core'; +import { ActivatedRoute } from '@angular/router'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { + NotificationDeliveryMethod, + NotificationDeliveryMethodTranslateMap, + NotificationUserSettings +} from '@shared/models/notification.models'; +import { NotificationService } from '@core/http/notification.service'; +import { DialogService } from '@core/services/dialog.service'; + +@Component({ + selector: 'tb-notification-settings', + templateUrl: './notification-settings.component.html', + styleUrls: ['./notification-settings.component.scss'] +}) +export class NotificationSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { + + notificationSettings: UntypedFormGroup; + + notificationDeliveryMethods: NotificationDeliveryMethod[]; + notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; + + allowNotificationDeliveryMethods: Array; + + constructor(protected store: Store, + private route: ActivatedRoute, + private translate: TranslateService, + private dialogService: DialogService, + private notificationService: NotificationService, + private fb: UntypedFormBuilder,) { + super(store); + } + + ngOnInit() { + this.notificationDeliveryMethods = this.getNotificationDeliveryMethods(); + + this.notificationService.getAvailableDeliveryMethods({ignoreLoading: true}).subscribe(allowMethods => { + this.allowNotificationDeliveryMethods = allowMethods; + }); + + this.buildNotificationSettingsForm(); + this.patchNotificationSettings(this.route.snapshot.data.userSettings); + } + + private getNotificationDeliveryMethods(): NotificationDeliveryMethod[] { + const deliveryMethods = new Set([ + NotificationDeliveryMethod.SLACK, + NotificationDeliveryMethod.MICROSOFT_TEAMS + ]); + return Object.values(NotificationDeliveryMethod).filter(type => !deliveryMethods.has(type)); + } + + private buildNotificationSettingsForm() { + this.notificationSettings = this.fb.group({ + prefs: this.fb.array([]) + }); + } + + private patchNotificationSettings(settings: NotificationUserSettings) { + const notificationSettingsControls: Array = []; + let preparedSettings; + if (settings.prefs) { + preparedSettings = this.prepareNotificationSettings(settings.prefs); + preparedSettings.forEach((setting) => { + setting.enabledDeliveryMethods = Object.assign( + this.notificationDeliveryMethods.reduce((a, v) => ({ ...a, [v]: true}), {}), + setting.enabledDeliveryMethods + ); + notificationSettingsControls.push(this.fb.control(setting, [Validators.required])); + }); + } + this.notificationSettings.setControl('prefs', this.fb.array(notificationSettingsControls), {emitEvent: false}); + } + + private prepareNotificationSettings(prefs: any) { + return Object.entries(prefs).map((value: any) => { + value[1].name = value[0]; + return value[1]; + }); + } + + resetSettings() { + this.dialogService.confirm( + this.translate.instant('notification.settings.reset-all-title'), + this.translate.instant('notification.settings.reset-all-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe( + result => { + if (result) { + const settings = this.prepareNotificationSettings(this.route.snapshot.data.userSettings.prefs); + const notificationSettingsControls: Array = []; + this.notificationSettings.reset({}); + if (settings) { + settings.forEach((setting) => { + setting.enabled = true; + setting.enabledDeliveryMethods = this.notificationDeliveryMethods.reduce((a, v) => ({ ...a, [v]: true}), {}); + notificationSettingsControls.push(this.fb.control(setting, [Validators.required])); + }); + } + this.notificationSettings.setControl('prefs', this.fb.array(notificationSettingsControls), {emitEvent: false}); + this.save(); + } + } + ); + } + + getChecked = (method: NotificationDeliveryMethod = null): boolean => { + const type = this.notificationSettings.get('prefs').value; + if (isDefinedAndNotNull(method)) { + return isDefinedAndNotNull(type) && type.every(resource => resource.enabledDeliveryMethods[method]); + } + return isDefinedAndNotNull(type) && type.every(resource => resource.enabled); + }; + + getSomeChecked = () => { + const type = this.notificationSettings.get('prefs').value; + return isDefinedAndNotNull(type) && type.some(resource => resource.enabled); + }; + + getIndeterminate = (deliveryMethod: NotificationDeliveryMethod = null): boolean => { + const type = this.notificationSettings.get('prefs').value; + if (isDefinedAndNotNull(type)) { + const checkedResource = isDefinedAndNotNull(deliveryMethod) ? + type.filter(resource => resource.enabledDeliveryMethods[deliveryMethod]) : + type.filter(resource => resource.enabled); + return checkedResource.length !== 0 && checkedResource.length !== type.length; + } + return false; + }; + + changeInstanceTypeCheckBox = (value: boolean, deliveryMethod: NotificationDeliveryMethod = null): void => { + const type = deepClone(this.notificationSettings.get('prefs').value); + if (isDefinedAndNotNull(deliveryMethod)) { + type.forEach(notificationType => notificationType.enabledDeliveryMethods[deliveryMethod] = value); + } else { + type.forEach(notificationType => notificationType.enabled = value); + } + this.notificationSettings.get('prefs').patchValue(type); + this.notificationSettings.markAsDirty(); + }; + + get notificationSettingsFormArray(): UntypedFormArray { + return this.notificationSettings.get('prefs') as UntypedFormArray; + } + + save(): void { + const settings = {prefs: {}}; + this.notificationSettings.getRawValue().prefs.forEach(value => { + const key = value.name; + delete value.name; + settings.prefs[key] = value; + }); + this.notificationService.saveNotificationUserSettings(settings).subscribe( + (userSettings) => { + this.notificationSettings.get('prefs').reset({}); + this.patchNotificationSettings(userSettings); + } + ); + } + + confirmForm(): UntypedFormGroup { + return this.notificationSettings; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts index b3fa00ef72..c2dc5355e8 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts @@ -43,6 +43,7 @@ export abstract class TemplateConfiguration extends DialogComponent< emailTemplateForm: FormGroup; smsTemplateForm: FormGroup; slackTemplateForm: FormGroup; + microsoftTeamsTemplateForm: FormGroup; notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[]; notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; @@ -95,17 +96,9 @@ export abstract class TemplateConfiguration extends DialogComponent< icon: this.fb.group({ enabled: [false], icon: [{value: 'notifications', disabled: true}, Validators.required], - color: ['#757575'] - }), - actionButtonConfig: this.fb.group({ - enabled: [false], - text: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]], - linkType: [ActionButtonLinkType.LINK], - link: [{value: '', disabled: true}, Validators.required], - dashboardId: [{value: null, disabled: true}, Validators.required], - dashboardState: [{value: null, disabled: true}], - setEntityIdInState: [{value: true, disabled: true}], + color: [{value: '#757575', disabled: true}] }), + actionButtonConfig: this.createButtonConfigForm() }) }); @@ -114,39 +107,10 @@ export abstract class TemplateConfiguration extends DialogComponent< ).subscribe((value) => { if (value) { this.webTemplateForm.get('additionalConfig.icon.icon').enable({emitEvent: false}); + this.webTemplateForm.get('additionalConfig.icon.color').enable({emitEvent: false}); } else { this.webTemplateForm.get('additionalConfig.icon.icon').disable({emitEvent: false}); - } - }); - - this.webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe((value) => { - if (value) { - this.webTemplateForm.get('additionalConfig.actionButtonConfig').enable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').updateValueAndValidity({onlySelf: true}); - } else { - this.webTemplateForm.get('additionalConfig.actionButtonConfig').disable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').enable({emitEvent: false}); - } - }); - - this.webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe((value) => { - const isEnabled = this.webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').value; - if (isEnabled) { - if (value === ActionButtonLinkType.LINK) { - this.webTemplateForm.get('additionalConfig.actionButtonConfig.link').enable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').disable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardState').disable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.setEntityIdInState').disable({emitEvent: false}); - } else { - this.webTemplateForm.get('additionalConfig.actionButtonConfig.link').disable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').enable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardState').enable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.setEntityIdInState').enable({emitEvent: false}); - } + this.webTemplateForm.get('additionalConfig.icon.color').disable({emitEvent: false}); } }); @@ -163,11 +127,19 @@ export abstract class TemplateConfiguration extends DialogComponent< body: ['', Validators.required] }); + this.microsoftTeamsTemplateForm = this.fb.group({ + subject: [''], + body: ['', Validators.required], + themeColor: [''], + button: this.createButtonConfigForm() + }); + this.deliveryMethodFormsMap = new Map([ [NotificationDeliveryMethod.WEB, this.webTemplateForm], [NotificationDeliveryMethod.EMAIL, this.emailTemplateForm], [NotificationDeliveryMethod.SMS, this.smsTemplateForm], - [NotificationDeliveryMethod.SLACK, this.slackTemplateForm] + [NotificationDeliveryMethod.SLACK, this.slackTemplateForm], + [NotificationDeliveryMethod.MICROSOFT_TEAMS, this.microsoftTeamsTemplateForm] ]); } @@ -198,4 +170,48 @@ export abstract class TemplateConfiguration extends DialogComponent< }); return deepTrim(template); } + + private createButtonConfigForm(): FormGroup { + const form = this.fb.group({ + enabled: [false], + text: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]], + linkType: [ActionButtonLinkType.LINK], + link: [{value: '', disabled: true}, Validators.required], + dashboardId: [{value: null, disabled: true}, Validators.required], + dashboardState: [{value: null, disabled: true}], + setEntityIdInState: [{value: true, disabled: true}], + }); + + form.get('enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + if (value) { + form.enable({emitEvent: false}); + form.get('linkType').updateValueAndValidity({onlySelf: true}); + } else { + form.disable({emitEvent: false}); + form.get('enabled').enable({emitEvent: false}); + } + }); + + form.get('linkType').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + const isEnabled = form.get('enabled').value; + if (isEnabled) { + if (value === ActionButtonLinkType.LINK) { + form.get('link').enable({emitEvent: false}); + form.get('dashboardId').disable({emitEvent: false}); + form.get('dashboardState').disable({emitEvent: false}); + form.get('setEntityIdInState').disable({emitEvent: false}); + } else { + form.get('link').disable({emitEvent: false}); + form.get('dashboardId').enable({emitEvent: false}); + form.get('dashboardState').enable({emitEvent: false}); + form.get('setEntityIdInState').enable({emitEvent: false}); + } + } + }); + return form; + } } diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html index 067258fa24..81ca756263 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html @@ -27,7 +27,6 @@ -
-
-
- +
+
+ {{ 'icon.icon' | translate }} -
- +
+ - +
-
-
- - {{ 'notification.action-button' | translate }} - -
-
- - notification.button-text - - - {{ 'notification.button-text-required' | translate }} - - - {{ 'notification.button-text-max-length' | translate : - {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} - }} - - -
-
- - notification.action-type - - - {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} - - - - - notification.link - - - {{ 'notification.link-required' | translate }} - - - - - - - - -
- - {{ 'notification.set-entity-from-notification' | translate }} - -
-
+
+
+ + + + + {{ 'notification.action-button' | translate }} + + + + +
+ + notification.button-text + + + {{ 'notification.button-text-required' | translate }} + + + {{ 'notification.button-text-max-length' | translate : + {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} + }} + + +
+
+ + notification.action-type + + + {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} + + + + + notification.link + + + {{ 'notification.link-required' | translate }} + + + + + + + + +
+ + {{ 'notification.set-entity-from-notification' | translate }} + +
+
+
@@ -259,10 +267,107 @@ + + {{ 'notification.delivery-method.microsoft-teams' | translate }} +
+ {{ 'notification.input-fields-support-templatization' | translate}} + +
+
+ + notification.subject + + + + notification.message + + + {{ 'notification.message-required' | translate }} + + +
+
+
notification.theme-color
+ +
+
+ + + + + {{ 'notification.action-button' | translate }} + + + + +
+ + notification.button-text + + + {{ 'notification.button-text-required' | translate }} + + + {{ 'notification.button-text-max-length' | translate : + {length: microsoftTeamsTemplateForm.get('button.text').getError('maxlength').requiredLength} + }} + + +
+
+ + notification.action-type + + + {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} + + + + + notification.link + + + {{ 'notification.link-required' | translate }} + + + + + + + + +
+ + {{ 'notification.set-entity-from-notification' | translate }} + +
+
+
+
+
+
-
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss index 1a5fa270a8..cf41bae5d0 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss @@ -16,13 +16,33 @@ @import "../../../../../../scss/constants"; @import "../../../../../../theme"; -:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { - width: 800px; +:host { + width: 840px; height: 100%; max-width: 100%; max-height: 100vh; - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: min-content 4px minmax(auto, 1fr) min-content min-content; +} + +:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { + .mat-mdc-dialog-content { + grid-row: 3; + display: flex; + flex-direction: column; + height: 100%; + padding: 0; + color: rgba(0, 0, 0, 0.87); + } + + .tb-dialog-actions { + grid-row: 5; + display: flex; + } + + .mat-divider { + grid-row: 4; + } .tb-title { font-size: 16px; @@ -70,6 +90,7 @@ .delivery-method-container { display: inline-flex; flex: 1 1 calc(50% - 8px); + max-width: calc(50% - 8px); padding: 16px 12px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 6px; @@ -80,28 +101,10 @@ } } } - - .additional-config-group { - padding: 16px 16px 4px; - margin-bottom: 12px; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 6px; - width: 100%; - height: 100%; - - .toggle { - margin-bottom: 12px; - } - } } :host ::ng-deep { .mat-mdc-dialog-content { - display: flex; - flex-direction: column; - height: 100%; - padding: 0 !important; - .mat-stepper-horizontal { display: flex; height: 100%; @@ -122,5 +125,13 @@ } } } + + .tb-form-panel .mat-expansion-panel.tb-settings { + padding: 11px 16px; + + & > .mat-expansion-panel-content > .mat-expansion-panel-body { + gap: 0; + } + } } } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts index c14e450745..334174194c 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts @@ -40,7 +40,7 @@ export class UserProfileResolver implements Resolve { } } -const routes: Routes = [ +export const profileRoutes: Routes = [ { path: 'profile', component: ProfileComponent, @@ -59,6 +59,13 @@ const routes: Routes = [ } ]; +const routes: Routes = [ + { + path: 'profile', + redirectTo: 'account/profile' + } +]; + @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html index fad97beb68..c560a6f06a 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html @@ -38,7 +38,7 @@
user.email - + {{ 'user.email-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts index 0633e696dd..cf6b3d34cb 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts @@ -30,6 +30,7 @@ import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions'; import { ActivatedRoute } from '@angular/router'; import { isDefinedAndNotNull } from '@core/utils'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { AuthService } from '@core/auth/auth.service'; @Component({ selector: 'tb-profile', @@ -47,6 +48,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir constructor(protected store: Store, private route: ActivatedRoute, private userService: UserService, + private authService: AuthService, private translate: TranslateService, public fb: UntypedFormBuilder) { super(store); @@ -94,6 +96,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir lastName: user.lastName, } })); this.store.dispatch(new ActionSettingsChangeLanguage({ userLang: user.additionalInfo.lang })); + this.authService.refreshJwtToken(false); } ); } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.html index bc72cd90c0..0da62e48bc 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.html @@ -29,16 +29,12 @@
-
- - -
+ +
-
-
-
- - rulenode.name - - + + rulenode.name + + - {{ 'rulenode.name-required' | translate }} - - - {{ 'rulenode.name-max-length' | translate }} - - -
- - {{ 'rulenode.debug-mode' | translate }} - - - {{ 'rulenode.singleton-mode' | translate }} - -
-
- - -
- - rulenode.rule-node-description - - -
+ {{ 'rulenode.name-required' | translate }} + + + {{ 'rulenode.name-max-length' | translate }} + + +
+ + {{ 'rulenode.debug-mode' | translate }} + + + {{ 'rulenode.singleton-mode' | translate }} +
-
+ + + +
+ + rulenode.rule-node-description + + +
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.scss index e3cf6699b2..827f77b865 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.scss +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.scss @@ -27,10 +27,7 @@ display: flex; flex-direction: column; gap: 8px; - - @media #{$mat-gt-sm} { - margin-bottom: 24px; - } + margin-bottom: 22px; } @media #{$mat-gt-sm} { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index f1d6001c88..5fce7f8e0b 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -22,11 +22,11 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models'; import { EntityType } from '@shared/models/entity-type.models'; import { Subscription } from 'rxjs'; -import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleNodeConfigComponent } from './rule-node-config.component'; import { Router } from '@angular/router'; import { RuleChainType } from '@app/shared/models/rule-chain.models'; import { ComponentClusteringMode } from '@shared/models/component-descriptor.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-rule-node', @@ -47,12 +47,11 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O ruleChainType: RuleChainType; @Input() - isEdit: boolean; - - @Input() - isReadOnly: boolean; + @coerceBoolean() + disabled = false; @Input() + @coerceBoolean() isAdd = false; @Output() @@ -70,7 +69,6 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O constructor(protected store: Store, private fb: UntypedFormBuilder, - private ruleChainService: RuleChainService, private router: Router) { super(store); this.ruleNodeFormGroup = this.fb.group({}); @@ -99,6 +97,9 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O } else { this.ruleNodeFormGroup = this.fb.group({}); } + if (this.disabled) { + this.ruleNodeFormGroup.disable({emitEvent: false}); + } } private updateRuleNode() { @@ -108,6 +109,9 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O } ngOnInit(): void { + if (this.disabled) { + this.ruleNodeFormGroup.disable({emitEvent: false}); + } } ngOnChanges(changes: SimpleChanges): void { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html index 5f5594cc1a..6911558ad4 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html @@ -46,12 +46,12 @@ matTooltipPosition="above"> search -
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 4382ee4682..811c9ad350 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 @@ -21,14 +21,9 @@ import { AppState } from '@core/core.state'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; -import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; -import { getCurrentAuthUser } from '@core/auth/auth.selectors'; -import { Authority } from '@shared/models/authority.enum'; export interface SaveWidgetTypeAsDialogResult { widgetName: string; - bundleId: string; - bundleAlias: string; } @Component({ @@ -41,26 +36,16 @@ export class SaveWidgetTypeAsDialogComponent extends saveWidgetTypeAsFormGroup: UntypedFormGroup; - bundlesScope: string; - constructor(protected store: Store, protected router: Router, public dialogRef: MatDialogRef, public fb: UntypedFormBuilder) { super(store, router, dialogRef); - - const authUser = getCurrentAuthUser(store); - if (authUser.authority === Authority.TENANT_ADMIN) { - this.bundlesScope = 'tenant'; - } else { - this.bundlesScope = 'system'; - } } ngOnInit(): void { this.saveWidgetTypeAsFormGroup = this.fb.group({ - title: [null, [Validators.required]], - widgetsBundle: [null, [Validators.required]] + title: [null, [Validators.required]] }); } @@ -70,11 +55,8 @@ export class SaveWidgetTypeAsDialogComponent extends saveAs(): void { const widgetName: string = this.saveWidgetTypeAsFormGroup.get('title').value; - const widgetsBundle: WidgetsBundle = this.saveWidgetTypeAsFormGroup.get('widgetsBundle').value; const result: SaveWidgetTypeAsDialogResult = { - widgetName, - bundleId: widgetsBundle.id.id, - bundleAlias: widgetsBundle.alias + widgetName }; this.dialogRef.close(result); } diff --git a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html index d15e8fbe1f..531d9d3cfe 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html @@ -34,13 +34,9 @@
diff --git a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss index 7de71586fa..d289574f15 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss @@ -21,7 +21,7 @@ display: flex; flex-direction: column; align-items: center; - mat-icon { + .mat-icon { margin: auto; } span.mdc-button__label { 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 2b650cb1ce..f19edc35c7 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 @@ -22,7 +22,7 @@ + placeholder="{{ 'widget.widget-title' | translate }}"/> - save + save_as action.saveAs @@ -253,6 +253,11 @@ rows="2" maxlength="255"> {{descriptionInput.value?.length || 0}}/255 + + {{ 'widget.deprecated' | translate }} + widget.settings-form-selector (); constructor(protected store: Store, @@ -181,14 +183,13 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe } private init(data: any) { - this.widgetsBundle = data.widgetsBundle; + this.widgetTypeDetails = data.widgetEditorData.widgetTypeDetails; + this.widget = data.widgetEditorData.widget; if (this.authUser.authority === Authority.TENANT_ADMIN) { - this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; + this.isReadOnly = this.widgetTypeDetails && this.widgetTypeDetails.tenantId.id === NULL_UUID; } else { this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; } - this.widgetTypeDetails = data.widgetEditorData.widgetTypeDetails; - this.widget = data.widgetEditorData.widget; if (this.widgetTypeDetails) { const config = JSON.parse(this.widget.defaultConfig); this.widget.defaultConfig = JSON.stringify(config); @@ -542,17 +543,22 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe private commitSaveWidget() { const id = (this.widgetTypeDetails && this.widgetTypeDetails.id) ? this.widgetTypeDetails.id : undefined; const createdTime = (this.widgetTypeDetails && this.widgetTypeDetails.createdTime) ? this.widgetTypeDetails.createdTime : undefined; - this.widgetService.saveWidgetTypeDetails(this.widget, id, this.widgetsBundle.alias, createdTime).subscribe( - (widgetTypeDetails) => { - this.setWidgetTypeDetails(widgetTypeDetails); + this.widgetService.saveWidgetTypeDetails(this.widget, id, createdTime).subscribe({ + next: (widgetTypeDetails) => { this.saveWidgetPending = false; + if (!this.widgetTypeDetails?.id) { + this.isDirty = false; + this.router.navigate(['..', widgetTypeDetails.id.id], {relativeTo: this.route}); + } else { + this.setWidgetTypeDetails(widgetTypeDetails); + } this.store.dispatch(new ActionNotificationShow( {message: this.translate.instant('widget.widget-saved'), type: 'success', duration: 500})); }, - () => { + error: () => { this.saveWidgetPending = false; } - ); + }); } private commitSaveWidgetAs() { @@ -564,18 +570,24 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe (saveWidgetAsData) => { if (saveWidgetAsData) { this.widget.widgetName = saveWidgetAsData.widgetName; - this.widget.alias = undefined; + this.widget.fullFqn = undefined; const config = JSON.parse(this.widget.defaultConfig); config.title = this.widget.widgetName; this.widget.defaultConfig = JSON.stringify(config); this.isDirty = false; - this.widgetService.saveWidgetTypeDetails(this.widget, undefined, saveWidgetAsData.bundleAlias, undefined).subscribe( - (widgetTypeDetails) => { - this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeDetails.id.id}`); - } - ); + this.widgetService.saveWidgetTypeDetails(this.widget, undefined, undefined).subscribe( + { + next: (widgetTypeDetails) => { + this.saveWidgetAsPending = false; + this.router.navigate(['..', widgetTypeDetails.id.id], {relativeTo: this.route}); + }, + error: () => { + this.saveWidgetAsPending = false; + } + }); + } else { + this.saveWidgetAsPending = false; } - this.saveWidgetAsPending = false; } ); } @@ -587,6 +599,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe this.widget.defaultConfig = JSON.stringify(config); this.origWidget = deepClone(this.widget); this.isDirty = false; + this.updateBreadcrumbs.emit(); } applyWidgetScript(): void { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts index 368ce8b138..4ac54e7a68 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts @@ -20,7 +20,6 @@ import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/ import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; import { Authority } from '@shared/models/authority.enum'; import { WidgetsBundlesTableConfigResolver } from '@modules/home/pages/widget/widgets-bundles-table-config.resolver'; -import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component'; import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; import { Observable } from 'rxjs'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; @@ -28,10 +27,11 @@ import { WidgetService } from '@core/http/widget.service'; import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; import { map } from 'rxjs/operators'; import { detailsToWidgetInfo, WidgetInfo } from '@home/models/widget-component.models'; -import { widgetType, WidgetTypeDetails } from '@app/shared/models/widget.models'; +import { widgetType, WidgetTypeDetails, WidgetTypeInfo } from '@app/shared/models/widget.models'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; -import { WidgetsData } from '@home/models/dashboard-component.models'; -import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { RouterTabsComponent } from '@home/components/router-tabs.component'; +import { WidgetTypesTableConfigResolver } from '@home/pages/widget/widget-types-table-config.resolver'; +import { WidgetsBundleWidgetsComponent } from '@home/pages/widget/widgets-bundle-widgets.component'; export interface WidgetEditorData { widgetTypeDetails: WidgetTypeDetails; @@ -45,30 +45,20 @@ export class WidgetsBundleResolver implements Resolve { } resolve(route: ActivatedRouteSnapshot): Observable { - let widgetsBundleId = route.params.widgetsBundleId; - if (!widgetsBundleId) { - widgetsBundleId = route.parent.params.widgetsBundleId; - } + const widgetsBundleId = route.params.widgetsBundleId; return this.widgetsService.getWidgetsBundle(widgetsBundleId); } } @Injectable() -export class WidgetsTypesDataResolver implements Resolve { +export class WidgetsBundleWidgetsResolver implements Resolve> { constructor(private widgetsService: WidgetService) { } - resolve(route: ActivatedRouteSnapshot): Observable { - const widgetsBundle: WidgetsBundle = route.parent.data.widgetsBundle; - const bundleAlias = widgetsBundle.alias; - const isSystem = widgetsBundle.tenantId.id === NULL_UUID; - return this.widgetsService.loadBundleLibraryWidgets(bundleAlias, - isSystem).pipe( - map((widgets) => { - return { widgets }; - } - )); + resolve(route: ActivatedRouteSnapshot): Observable> { + const widgetsBundleId = route.params.widgetsBundleId; + return this.widgetsService.getBundleWidgetTypeInfos(widgetsBundleId); } } @@ -80,47 +70,82 @@ export class WidgetEditorDataResolver implements Resolve { resolve(route: ActivatedRouteSnapshot): Observable { const widgetTypeId = route.params.widgetTypeId; - return this.widgetsService.getWidgetTypeById(widgetTypeId).pipe( - map((result) => { - return { + if (!widgetTypeId || Object.keys(widgetType).includes(widgetTypeId)) { + let widgetTypeParam = widgetTypeId as widgetType; + if (!widgetTypeParam) { + widgetTypeParam = widgetType.timeseries; + } + return this.widgetsService.getWidgetTemplate(widgetTypeParam).pipe( + map((widget) => { + widget.widgetName = null; + return { + widgetTypeDetails: null, + widget + }; + }) + ); + } else { + return this.widgetsService.getWidgetTypeById(widgetTypeId).pipe( + map((result) => ({ widgetTypeDetails: result, widget: detailsToWidgetInfo(result) - }; - }) - ); - } -} - -@Injectable() -export class WidgetEditorAddDataResolver implements Resolve { - - constructor(private widgetsService: WidgetService) { - } - - resolve(route: ActivatedRouteSnapshot): Observable { - let widgetTypeParam = route.params.widgetType as widgetType; - if (!widgetTypeParam) { - widgetTypeParam = widgetType.timeseries; + })) + ); } - return this.widgetsService.getWidgetTemplate(widgetTypeParam).pipe( - map((widget) => { - widget.widgetName = null; - return { - widgetTypeDetails: null, - widget - }; - }) - ); } } -export const widgetTypesBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate) => +export const widgetsBundleWidgetsBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate) => route.data.widgetsBundle.title); export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction = - ((route, translate, component) => component ? component.widget.widgetName : ''); + ((route, translate, component) => + component?.widget?.widgetName ? + (component.widget.widgetName + (component.widget.deprecated ? ` (${translate.instant('widget.deprecated')})` : '')) : ''); -export const widgetsBundlesRoutes: Routes = [ +const widgetTypesRoutes: Routes = [ + { + path: 'widget-types', + data: { + breadcrumb: { + label: 'widget.widgets', + icon: 'now_widgets' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + title: 'widget.widgets' + }, + resolve: { + entitiesTableConfig: WidgetTypesTableConfigResolver + } + }, + { + path: ':widgetTypeId', + component: WidgetEditorComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + title: 'widget.editor', + breadcrumb: { + labelFunction: widgetEditorBreadcumbLabelFunction, + icon: 'insert_chart' + } as BreadCrumbConfig, + hideTabs: true + }, + resolve: { + widgetEditorData: WidgetEditorDataResolver + } + } + ] + }, +]; + +const widgetsBundlesRoutes: Routes = [ { path: 'widgets-bundles', data: { @@ -142,85 +167,91 @@ export const widgetsBundlesRoutes: Routes = [ } }, { - path: ':widgetsBundleId/widgetTypes', + path: ':widgetsBundleId', + component: WidgetsBundleWidgetsComponent, + canDeactivate: [ConfirmOnExitGuard], data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + title: 'widgets-bundle.widgets-bundle-widgets', breadcrumb: { - labelFunction: widgetTypesBreadcumbLabelFunction, + labelFunction: widgetsBundleWidgetsBreadcumbLabelFunction, icon: 'now_widgets' - } as BreadCrumbConfig + } as BreadCrumbConfig, + hideTabs: true }, resolve: { - widgetsBundle: WidgetsBundleResolver - }, - children: [ - { - path: '', - component: WidgetLibraryComponent, - data: { - auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], - title: 'widget.widget-library' - }, - resolve: { - widgetsData: WidgetsTypesDataResolver - } - }, - { - path: ':widgetTypeId', - component: WidgetEditorComponent, - canDeactivate: [ConfirmOnExitGuard], - data: { - auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], - title: 'widget.editor', - breadcrumb: { - labelFunction: widgetEditorBreadcumbLabelFunction, - icon: 'insert_chart' - } as BreadCrumbConfig - }, - resolve: { - widgetEditorData: WidgetEditorDataResolver - } - }, - { - path: 'add/:widgetType', - component: WidgetEditorComponent, - canDeactivate: [ConfirmOnExitGuard], - data: { - auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], - title: 'widget.editor', - breadcrumb: { - labelFunction: widgetEditorBreadcumbLabelFunction, - icon: 'insert_chart' - } as BreadCrumbConfig - }, - resolve: { - widgetEditorData: WidgetEditorAddDataResolver - } - } - ] + widgetsBundle: WidgetsBundleResolver, + widgets: WidgetsBundleWidgetsResolver + } } ] }, ]; +export const widgetsLibraryRoutes: Routes = [ + { + path: 'widgets-library', + component: RouterTabsComponent, + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + breadcrumb: { + label: 'widget.widget-library', + icon: 'now_widgets' + } + }, + children: [ + { + path: '', + children: [], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + redirectTo: '/resources/widgets-library/widget-types' + } + }, + ...widgetTypesRoutes, + ...widgetsBundlesRoutes + ] + } +]; + const routes: Routes = [ { path: 'widgets-bundles', pathMatch: 'full', - redirectTo: '/resources/widgets-bundles' + redirectTo: '/resources/widgets-library/widgets-bundles' + }, + { + path: 'resources/widgets-bundles', + pathMatch: 'full', + redirectTo: '/resources/widgets-library/widgets-bundles' }, { path: 'widgets-bundles/:widgetsBundleId/widgetTypes', pathMatch: 'full', - redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes', + redirectTo: '/resources/widgets-library/widgets-bundles/:widgetsBundleId' + }, + { + path: 'resources/widgets-bundles/:widgetsBundleId/widgetTypes', + pathMatch: 'full', + redirectTo: '/resources/widgets-library/widgets-bundles/:widgetsBundleId' }, { path: 'widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId', pathMatch: 'full', - redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId', + redirectTo: '/resources/widgets-library/widget-types/:widgetTypeId' + }, + { + path: 'resources/widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId', + pathMatch: 'full', + redirectTo: '/resources/widgets-library/widget-types/:widgetTypeId' }, { path: 'widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType', - redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType', + redirectTo: '/resources/widgets-library/widget-types/:widgetType', + }, + { + path: 'resources/widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType', + redirectTo: '/resources/widgets-library/widget-types/:widgetType', } ]; @@ -229,11 +260,11 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], providers: [ + WidgetTypesTableConfigResolver, WidgetsBundlesTableConfigResolver, WidgetsBundleResolver, - WidgetsTypesDataResolver, - WidgetEditorDataResolver, - WidgetEditorAddDataResolver + WidgetsBundleWidgetsResolver, + WidgetEditorDataResolver ] }) export class WidgetLibraryRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html deleted file mode 100644 index d6d7fb4869..0000000000 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html +++ /dev/null @@ -1,42 +0,0 @@ - -
- - widgets-bundle.empty -
- - - diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts deleted file mode 100644 index 970fa8ebe6..0000000000 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts +++ /dev/null @@ -1,208 +0,0 @@ -/// -/// Copyright © 2016-2023 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, OnInit, ViewChild } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { PageComponent } from '@shared/components/page.component'; -import { AuthUser } from '@shared/models/user.model'; -import { getCurrentAuthUser } from '@core/auth/auth.selectors'; -import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Authority } from '@shared/models/authority.enum'; -import { NULL_UUID } from '@shared/models/id/has-uuid'; -import { of } from 'rxjs'; -import { Widget, widgetType } from '@app/shared/models/widget.models'; -import { WidgetService } from '@core/http/widget.service'; -import { map, mergeMap } from 'rxjs/operators'; -import { DialogService } from '@core/services/dialog.service'; -import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; -import { DashboardCallbacks, IDashboardComponent, WidgetsData } from '@home/models/dashboard-component.models'; -import { IAliasController, IStateController, StateParams } from '@app/core/api/widget-api.models'; -import { AliasController } from '@core/api/alias-controller'; -import { MatDialog } from '@angular/material/dialog'; -import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; -import { TranslateService } from '@ngx-translate/core'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; -import { UtilsService } from '@core/services/utils.service'; -import { EntityService } from '@core/http/entity.service'; - -@Component({ - selector: 'tb-widget-library', - templateUrl: './widget-library.component.html', - styleUrls: ['./widget-library.component.scss'] -}) -export class WidgetLibraryComponent extends PageComponent implements OnInit { - - authUser: AuthUser; - - isReadOnly: boolean; - - widgetsBundle: WidgetsBundle; - - widgetsData: WidgetsData; - - footerFabButtons: FooterFabButtons = { - fabTogglerName: 'widget.add-widget-type', - fabTogglerIcon: 'add', - buttons: [ - { - name: 'widget-type.create-new-widget-type', - icon: 'insert_drive_file', - onAction: ($event) => { - this.addWidgetType($event); - } - }, - { - name: 'widget-type.import', - icon: 'file_upload', - onAction: ($event) => { - this.importWidgetType($event); - } - } - ] - }; - - dashboardCallbacks: DashboardCallbacks = { - onEditWidget: this.openWidgetType.bind(this), - onWidgetClicked: this.openWidgetType.bind(this), - onExportWidget: this.exportWidgetType.bind(this), - onRemoveWidget: this.removeWidgetType.bind(this) - }; - - aliasController: IAliasController = new AliasController(this.utils, - this.entityService, - this.translate, - () => { return { - getStateParams(): StateParams { - return {}; - } - } as IStateController; - }, - {}, - {}); - - @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; - - constructor(protected store: Store, - private route: ActivatedRoute, - private router: Router, - private widgetService: WidgetService, - private dialogService: DialogService, - private importExport: ImportExportService, - private dialog: MatDialog, - private translate: TranslateService, - private utils: UtilsService, - private entityService: EntityService) { - super(store); - - this.authUser = getCurrentAuthUser(this.store); - this.widgetsBundle = this.route.snapshot.data.widgetsBundle; - this.widgetsData = this.route.snapshot.data.widgetsData; - if (this.authUser.authority === Authority.TENANT_ADMIN) { - this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; - } else { - this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; - } - } - - ngOnInit(): void { - } - - addWidgetType($event: Event): void { - this.openWidgetType($event); - } - - importWidgetType($event: Event): void { - if ($event) { - $event.stopPropagation(); - } - this.importExport.importWidgetType(this.widgetsBundle.alias).subscribe( - (widgetTypeInstance) => { - if (widgetTypeInstance) { - this.reload(); - } - } - ); - } - - private reload() { - const bundleAlias = this.widgetsBundle.alias; - const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID; - this.widgetService.loadBundleLibraryWidgets(bundleAlias, isSystem).subscribe( - (widgets) => { - this.widgetsData = {widgets}; - } - ); - } - - openWidgetType($event: Event, widget?: Widget): void { - if ($event) { - $event.stopPropagation(); - } - if (widget) { - this.router.navigate([widget.typeId.id], {relativeTo: this.route}); - } else { - this.dialog.open(SelectWidgetTypeDialogComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] - }).afterClosed().subscribe( - (type) => { - if (type) { - this.router.navigate(['add', type], {relativeTo: this.route}); - } - } - ); - } - } - - exportWidgetType($event: Event, widget: Widget): void { - if ($event) { - $event.stopPropagation(); - } - this.importExport.exportWidgetType(widget.typeId.id); - } - - removeWidgetType($event: Event, widget: Widget): void { - if ($event) { - $event.stopPropagation(); - } - this.dialogService.confirm( - this.translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title}), - this.translate.instant('widget.remove-widget-type-text'), - this.translate.instant('action.no'), - this.translate.instant('action.yes'), - ).pipe( - mergeMap((result) => { - if (result) { - return this.widgetService.deleteWidgetType(widget.bundleAlias, widget.typeAlias, widget.isSystemType); - } else { - return of(false); - } - }), - map((result) => { - if (result !== false) { - this.reload(); - return true; - } else { - return false; - } - } - )).subscribe(); - } - -} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts index 6a6e4a47e0..b9723c15d8 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts @@ -20,19 +20,26 @@ import { SharedModule } from '@shared/shared.module'; import { WidgetsBundleComponent } from '@modules/home/pages/widget/widgets-bundle.component'; import { WidgetLibraryRoutingModule } from '@modules/home/pages/widget/widget-library-routing.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; -import { WidgetLibraryComponent } from './widget-library.component'; import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; import { SaveWidgetTypeAsDialogComponent } from './save-widget-type-as-dialog.component'; import { WidgetsBundleTabsComponent } from '@home/pages/widget/widgets-bundle-tabs.component'; +import { WidgetTypeComponent } from '@home/pages/widget/widget-type.component'; +import { WidgetTypeTabsComponent } from '@home/pages/widget/widget-type-tabs.component'; +import { WidgetsBundleWidgetsComponent } from '@home/pages/widget/widgets-bundle-widgets.component'; +import { WidgetTypeAutocompleteComponent } from '@home/pages/widget/widget-type-autocomplete.component'; @NgModule({ declarations: [ + WidgetTypeComponent, WidgetsBundleComponent, - WidgetLibraryComponent, + // WidgetLibraryComponent, + WidgetTypeAutocompleteComponent, + WidgetsBundleWidgetsComponent, WidgetEditorComponent, SelectWidgetTypeDialogComponent, SaveWidgetTypeAsDialogComponent, + WidgetTypeTabsComponent, WidgetsBundleTabsComponent ], imports: [ diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html new file mode 100644 index 0000000000..4c8087f39b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html @@ -0,0 +1,59 @@ + + + {{ label }} + + + + +
+ {{ widgetType.name }} +
+
+
{{ 'widget.deprecated' | translate }}
+
+
+
+ + + {{ translate.get('widget.no-widgets-matching', {entity: searchText}) | async }} + + +
+ + + + + + +
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.scss b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.scss new file mode 100644 index 0000000000..79a5bc5c80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2023 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-widget-type-autocomplete { + .mat-mdc-option { + .mdc-list-item__primary-text { + width: 100%; + .tb-widget-type-option-container { + display: flex; + gap: 8px; + align-items: center; + } + .tb-widget-type-option-image-preview { + width: 36px; + max-height: 100%; + object-fit: contain; + border-radius: 6px; + } + .tb-widget-type-option-details { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + .tb-widget-type-option-text { + font-size: 14px; + font-weight: 400; + letter-spacing: 0.2px; + } + .tb-widget-type-option-deprecated { + font-size: 13px; + font-weight: 400; + color: rgba(0, 0, 0, 0.54); + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.ts new file mode 100644 index 0000000000..1bb5fa0d17 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.ts @@ -0,0 +1,227 @@ +/// +/// Copyright © 2016-2023 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, + ElementRef, + forwardRef, + Input, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; +import { emptyPageData } from '@shared/models/page/page-data'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; +import { WidgetTypeInfo } from '@shared/models/widget.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { WidgetService } from '@core/http/widget.service'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { isDefinedAndNotNull } from '@core/utils'; + +@Component({ + selector: 'tb-widget-type-autocomplete', + templateUrl: './widget-type-autocomplete.component.html', + styleUrls: ['./widget-type-autocomplete.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetTypeAutocompleteComponent), + multi: true + }], + encapsulation: ViewEncapsulation.None +}) +export class WidgetTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + private dirty = false; + + selectWidgetTypeFormGroup: UntypedFormGroup; + + modelValue: WidgetTypeInfo | null; + + @Input() + label = this.translate.instant('widget.widget'); + + @Input() + placeholder: string; + + @Input() + floatLabel: FloatLabelType = 'auto'; + + @Input() + appearance: MatFormFieldAppearance = 'fill'; + + @Input() + subscriptSizing: SubscriptSizing = 'fixed'; + + @Input() + @coerceBoolean() + required: boolean; + + @Input() + disabled: boolean; + + @Input() + excludeWidgetTypeIds: Array; + + @ViewChild('widgetTypeInput', {static: true}) widgetTypeInput: ElementRef; + + filteredWidgetTypes: Observable>; + + searchText = ''; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private widgetService: WidgetService, + private sanitizer: DomSanitizer, + private fb: UntypedFormBuilder) { + this.selectWidgetTypeFormGroup = this.fb.group({ + widgetType: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredWidgetTypes = this.selectWidgetTypeFormGroup.get('widgetType').valueChanges + .pipe( + debounceTime(150), + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value; + } + this.updateView(modelValue); + }), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + distinctUntilChanged(), + switchMap(name => this.fetchWidgetTypes(name) ), + share() + ); + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectWidgetTypeFormGroup.disable({emitEvent: false}); + } else { + this.selectWidgetTypeFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: WidgetTypeInfo | string | null): void { + this.searchText = ''; + if (value != null) { + if (typeof value === 'string') { + this.widgetService.getWidgetTypeInfoById(value).subscribe( + (widgetType) => { + this.modelValue = widgetType; + this.selectWidgetTypeFormGroup.get('widgetType').patchValue(widgetType, {emitEvent: false}); + } + ); + } else { + this.modelValue = value; + this.selectWidgetTypeFormGroup.get('widgetType').patchValue(value, {emitEvent: false}); + } + } else { + this.modelValue = null; + this.selectWidgetTypeFormGroup.get('widgetType').patchValue('', {emitEvent: false}); + } + this.dirty = true; + } + + updateView(value: WidgetTypeInfo | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayWidgetTypeFn(widgetType?: WidgetTypeInfo): string | undefined { + return widgetType ? widgetType.name : undefined; + } + + getPreviewImage(imageUrl: string | null): SafeUrl | string { + if (isDefinedAndNotNull(imageUrl)) { + return this.sanitizer.bypassSecurityTrustUrl(imageUrl); + } + return '/assets/widget-preview-empty.svg'; + } + + fetchWidgetTypes(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(10, 0, searchText, { + property: 'name', + direction: Direction.ASC + }); + return this.getWidgetTypes(pageLink); + } + + getWidgetTypes(pageLink: PageLink, + result: Array = []): Observable> { + return this.widgetService.getWidgetTypes(pageLink, true).pipe( + catchError(() => of(emptyPageData())), + switchMap((data) => { + if (this.excludeWidgetTypeIds?.length) { + const filtered = data.data.filter(w => !this.excludeWidgetTypeIds.includes(w.id.id)); + result = result.concat(filtered); + } else { + result = data.data; + } + if (result.length >= pageLink.pageSize || !this.excludeWidgetTypeIds?.length || !data.hasNext) { + return of(result); + } else { + return this.getWidgetTypes(pageLink.nextPageLink(), result); + } + }) + ); + } + + onFocus() { + if (this.dirty) { + this.selectWidgetTypeFormGroup.get('widgetType').updateValueAndValidity({onlySelf: true}); + this.dirty = false; + } + } + + clear() { + this.selectWidgetTypeFormGroup.get('widgetType').patchValue(''); + setTimeout(() => { + this.widgetTypeInput.nativeElement.blur(); + this.widgetTypeInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.html new file mode 100644 index 0000000000..b6a6ecc32b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.html @@ -0,0 +1,23 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.ts new file mode 100644 index 0000000000..6543777295 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2023 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 { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { WidgetTypeDetails } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-widget-type-tabs', + templateUrl: './widget-type-tabs.component.html', + styleUrls: [] +}) +export class WidgetTypeTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + isTenantWidgetType() { + return this.entity && this.entity.tenantId.id !== NULL_UUID; + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html new file mode 100644 index 0000000000..cacd187b5b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html @@ -0,0 +1,66 @@ + +
+ + + +
+
+
+
+ + widget.title + + + {{ 'widget.title-required' | translate }} + + + {{ 'widget.title-max-length' | translate }} + + + + + + widget.description + + {{descriptionInput.value?.length || 0}}/255 + + + {{ '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 new file mode 100644 index 0000000000..bdbfebbd8f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts @@ -0,0 +1,68 @@ +/// +/// Copyright © 2016-2023 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, Inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '../../components/entity/entity.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { WidgetTypeDetails } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-widget-type', + templateUrl: './widget-type.component.html', + styleUrls: [] +}) +export class WidgetTypeComponent extends EntityComponent { + + constructor(protected store: Store, + @Inject('entity') protected entityValue: WidgetTypeDetails, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: UntypedFormBuilder, + protected cd: ChangeDetectorRef) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: WidgetTypeDetails): UntypedFormGroup { + return this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], + image: [entity ? entity.image : ''], + description: [entity ? entity.description : '', Validators.maxLength(255)], + deprecated: [entity ? entity.deprecated : false] + } + ); + } + + updateForm(entity: WidgetTypeDetails) { + this.entityForm.patchValue({ + name: entity.name, + image: entity.image, + description: entity.description, + deprecated: entity.deprecated + }); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts new file mode 100644 index 0000000000..8fcf76f1fe --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts @@ -0,0 +1,231 @@ +/// +/// Copyright © 2016-2023 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 { Injectable } from '@angular/core'; + +import { Resolve, Router } from '@angular/router'; +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { WidgetService } from '@app/core/http/widget.service'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { getCurrentAuthState, getCurrentAuthUser } from '@app/core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { DialogService } from '@core/services/dialog.service'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { Direction } from '@shared/models/page/sort-order'; +import { + BaseWidgetType, + WidgetTypeDetails, + WidgetTypeInfo, + widgetType as WidgetDataType, + widgetTypesData +} from '@shared/models/widget.models'; +import { WidgetTypeComponent } from '@home/pages/widget/widget-type.component'; +import { WidgetTypeTabsComponent } from '@home/pages/widget/widget-type-tabs.component'; +import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; + +@Injectable() +export class WidgetTypesTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = + new EntityTableConfig(); + + constructor(private store: Store, + private dialog: MatDialog, + private widgetsService: WidgetService, + private translate: TranslateService, + private importExport: ImportExportService, + private datePipe: DatePipe, + private router: Router) { + + this.config.entityType = EntityType.WIDGETS_BUNDLE; + this.config.entityComponent = WidgetTypeComponent; + this.config.entityTabsComponent = WidgetTypeTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.WIDGET_TYPE); + this.config.entityResources = entityTypeResources.get(EntityType.WIDGET_TYPE); + this.config.defaultSortOrder = {property: 'name', direction: Direction.ASC}; + + this.config.rowPointer = true; + + this.config.entityTitle = (widgetType) => widgetType ? + widgetType.name : ''; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'widget.title', '100%'), + new EntityTableColumn('widgetType', 'widget.type', '150px', entity => + entity?.widgetType ? this.translate.instant(widgetTypesData.get(entity.widgetType).name) : '', undefined, false), + new EntityTableColumn('tenantId', 'widget.system', '60px', + entity => checkBoxCell(entity.tenantId.id === NULL_UUID)), + new EntityTableColumn('deprecated', 'widget.deprecated', '60px', + entity => checkBoxCell(entity.deprecated)) + ); + + this.config.addActionDescriptors.push( + { + name: this.translate.instant('widget-type.create-new-widget-type'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.addWidgetType($event) + }, + { + name: this.translate.instant('widget-type.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: ($event) => this.importWidgetType($event) + } + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('widget-type.export'), + icon: 'file_download', + isEnabled: () => true, + onAction: ($event, entity) => this.exportWidgetType($event, entity) + }, + { + name: this.translate.instant('widget.widget-type-details'), + icon: 'edit', + isEnabled: () => true, + onAction: ($event, entity) => this.config.toggleEntityDetails($event, entity) + } + ); + + this.config.groupActionDescriptors.push( + { + name: this.translate.instant('widget-type.export-widget-types'), + icon: 'file_download', + isEnabled: true, + onAction: ($event, entities) => this.exportWidgetTypes($event, entities) + } + ); + + this.config.deleteEntityTitle = widgetType => this.translate.instant('widget.delete-widget-type-title', + { widgetTypeName: widgetType.name }); + this.config.deleteEntityContent = () => this.translate.instant('widget.delete-widget-type-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('widget.delete-widget-types-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('widget.delete-widget-types-text'); + + + this.config.loadEntity = id => this.widgetsService.getWidgetTypeById(id.id); + this.config.saveEntity = widgetType => this.widgetsService.saveWidgetType(widgetType as WidgetTypeDetails); + this.config.deleteEntity = id => this.widgetsService.deleteWidgetType(id.id); + this.config.onEntityAction = action => this.onWidgetTypeAction(action); + + this.config.handleRowClick = ($event, widgetType) => { + if (this.config.isDetailsOpen()) { + this.config.toggleEntityDetails($event, widgetType); + } else { + this.openWidgetEditor($event, widgetType); + } + return true; + }; + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('widget.widget-types'); + const authUser = getCurrentAuthUser(this.store); + this.config.deleteEnabled = (widgetType) => this.isWidgetTypeEditable(widgetType, authUser.authority); + this.config.entitySelectionEnabled = (widgetType) => this.isWidgetTypeEditable(widgetType, authUser.authority); + this.config.detailsReadonly = (widgetType) => !this.isWidgetTypeEditable(widgetType, authUser.authority); + this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetTypes(pageLink); + return this.config; + } + + isWidgetTypeEditable(widgetType: BaseWidgetType, authority: Authority): boolean { + if (authority === Authority.TENANT_ADMIN) { + return widgetType && widgetType.tenantId && widgetType.tenantId.id !== NULL_UUID; + } else { + return authority === Authority.SYS_ADMIN; + } + } + + addWidgetType($event: Event): void { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(SelectWidgetTypeDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (type) => { + if (type) { + this.router.navigateByUrl(`resources/widgets-library/widget-types/${type}`); + } + } + ); + } + + importWidgetType($event: Event) { + this.importExport.importWidgetType().subscribe( + (widgetType) => { + if (widgetType) { + this.config.updateData(); + } + } + ); + } + + openWidgetEditor($event: Event, widgetType: BaseWidgetType) { + if ($event) { + $event.stopPropagation(); + } + this.router.navigateByUrl(`resources/widgets-library/widget-types/${widgetType.id.id}`); + } + + exportWidgetType($event: Event, widgetType: BaseWidgetType) { + if ($event) { + $event.stopPropagation(); + } + this.importExport.exportWidgetType(widgetType.id.id); + } + + exportWidgetTypes($event: Event, widgetTypes: BaseWidgetType[]) { + if ($event) { + $event.stopPropagation(); + } + this.importExport.exportWidgetTypes(widgetTypes.map(w => w.id.id)).subscribe( + () => { + this.config.getTable().clearSelection(); + } + ); + } + + onWidgetTypeAction(action: EntityAction): boolean { + switch (action.action) { + case 'edit': + this.openWidgetEditor(action.event, action.entity); + return true; + case 'export': + this.exportWidgetType(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html new file mode 100644 index 0000000000..2d95fd1fdb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html @@ -0,0 +1,134 @@ + + + + +
+ + {{ widgetsBundle.title }}: {{ 'widget.widgets' | translate }} + + +
+
+
+ + +
+ +
+
+
+ {{ widget.name }} +
+
{{ widget.name }}
+
{{ 'widget.deprecated' | translate }}
+
+
+ + + + +
+
+
+ add +
+
+
+ add +
widget.add
+
+
+
+
+
widget.no-widgets
+
+
+ + +
+ +
+
+
+ + + + + +
+ diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.scss b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.scss new file mode 100644 index 0000000000..3c971105ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.scss @@ -0,0 +1,184 @@ +/** + * Copyright © 2016-2023 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"; + +:host { + width: 100%; + height: 100%; + padding: 8px; +} + +:host ::ng-deep { + .tb-bundle-widgets-card { + .mat-mdc-card-header-text { + flex: 1; + } + } +} + +.tb-bundle-widgets-card { + padding: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + + @media #{$mat-md} { + width: 80%; + } + + @media #{$mat-gt-md} { + width: 60%; + } + + .title-container { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + .mat-headline-5 { + margin: 0; + } + } + + .mat-mdc-card-content { + flex: 1; + padding: 16px; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; + gap: 8px; + } + + .tb-add-widget-button { + min-height: 56px; + cursor: pointer; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: #FFFFFF; + padding: 8px; + border: 2px dashed rgba(0, 0, 0, 0.08); + border-radius: 10px; + } + + .tb-add-widget-button-panel { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + .tb-add-widget-button-with-text { + cursor: pointer; + display: flex; + padding: 24px 40px 24px 24px; + justify-content: center; + align-items: center; + gap: 8px; + border: 2px dashed rgba(0, 0, 0, 0.12); + border-radius: 10px; + .tb-add-widget-button-text { + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: $tb-primary-color; + } + } + } + + .tb-add-bundle-widget-form { + display: flex; + padding: 16px; + flex-direction: column; + gap: 12px; + border-radius: 10px; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.04); + } + + .tb-add-bundle-widget-actions { + display: flex; + justify-content: flex-end; + align-items: flex-start; + gap: 16px; + } + + .mat-mdc-card-actions { + justify-content: end; + gap: 8px; + } +} + +.tb-bundle-widgets-container { + display: flex; + flex-direction: column; + gap: 8px; + min-height: 0; + overflow: auto; + padding-top: 8px; + padding-bottom: 8px; +} + +.tb-bundle-widget-row { + display: flex; + flex-direction: row; + align-items: center; + padding: 6px 12px 6px 6px; + background: #fff; + gap: 12px; + border-radius: 10px; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.04); + .tb-bundle-widget-row-details { + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + .tb-bundle-widget-image-preview { + width: 64px; + max-height: 100%; + object-fit: contain; + border-radius: 6px; + } + .tb-bundle-widget-details { + padding: 18px 0; + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + .tb-bundle-widget-title { + font-size: 14px; + font-weight: 400; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + } + .tb-bundle-widget-deprecated { + font-size: 13px; + font-weight: 400; + color: rgba(0, 0, 0, 0.54); + } + } + } + .mat-icon { + color: rgba(0, 0, 0, 0.54); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts new file mode 100644 index 0000000000..0d21c6e556 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts @@ -0,0 +1,178 @@ +/// +/// Copyright © 2016-2023 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, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { AuthUser } from '@shared/models/user.model'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ActivatedRoute, Router } from '@angular/router'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { WidgetTypeInfo } from '@shared/models/widget.models'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { WidgetService } from '@core/http/widget.service'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { isDefinedAndNotNull } from '@core/utils'; +import { FormControl, Validators } from '@angular/forms'; + +@Component({ + selector: 'tb-widgets-bundle-widget', + templateUrl: './widgets-bundle-widgets.component.html', + styleUrls: ['./widgets-bundle-widgets.component.scss'] +}) +export class WidgetsBundleWidgetsComponent extends PageComponent implements OnInit { + + authUser: AuthUser; + + isReadOnly: boolean; + editMode = false; + addMode = false; + isDirty = false; + + widgetsBundle: WidgetsBundle; + widgets: Array; + excludeWidgetTypeIds: Array; + + addWidgetFormControl = new FormControl(null, [Validators.required]); + + constructor(protected store: Store, + private router: Router, + private route: ActivatedRoute, + private widgetsService: WidgetService, + private importExport: ImportExportService, + private sanitizer: DomSanitizer, + private cd: ChangeDetectorRef) { + super(store); + this.authUser = getCurrentAuthUser(this.store); + this.widgetsBundle = this.route.snapshot.data.widgetsBundle; + this.widgets = [...this.route.snapshot.data.widgets]; + if (this.authUser.authority === Authority.TENANT_ADMIN) { + this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; + } else { + this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; + } + if (!this.isReadOnly && !this.widgets.length) { + this.editMode = true; + } + this.addWidgetFormControl.valueChanges.subscribe((newWidget) => { + if (newWidget) { + this.addWidget(newWidget); + } + }); + } + + ngOnInit(): void { + } + + getPreviewImage(imageUrl: string | null): SafeUrl | string { + if (isDefinedAndNotNull(imageUrl)) { + return this.sanitizer.bypassSecurityTrustUrl(imageUrl); + } + return '/assets/widget-preview-empty.svg'; + } + + trackByWidget(index: number, widget: WidgetTypeInfo): any { + return widget; + } + + widgetDrop(event: CdkDragDrop) { + const widget = this.widgets[event.previousIndex]; + this.widgets.splice(event.previousIndex, 1); + this.widgets.splice(event.currentIndex, 0, widget); + this.isDirty = true; + } + + addWidgetMode() { + this.addWidgetFormControl.patchValue(null, {emitEvent: false}); + this.excludeWidgetTypeIds = this.widgets.map(w => w.id.id); + this.addMode = true; + } + + cancelAddWidgetMode() { + this.addMode = false; + } + + private addWidget(newWidget: WidgetTypeInfo) { + this.widgets.push(newWidget); + this.isDirty = true; + this.addMode = false; + } + + openWidgetEditor($event: Event, widgetType: WidgetTypeInfo) { + if ($event) { + $event.stopPropagation(); + } + this.router.navigateByUrl(`resources/widgets-library/widget-types/${widgetType.id.id}`); + } + + exportWidgetType($event: Event, widgetType: WidgetTypeInfo) { + if ($event) { + $event.stopPropagation(); + } + this.importExport.exportWidgetType(widgetType.id.id); + } + + removeWidgetType($event: Event, widgetType: WidgetTypeInfo) { + if ($event) { + $event.stopPropagation(); + } + const index = this.widgets.indexOf(widgetType); + this.widgets.splice(index, 1); + this.isDirty = true; + } + + goBack() { + this.router.navigate(['..'], { relativeTo: this.route }); + } + + exportWidgetsBundle() { + this.importExport.exportWidgetsBundle(this.widgetsBundle.id.id); + } + + edit() { + this.editMode = true; + } + + cancel() { + if (this.isDirty) { + this.widgetsService.getBundleWidgetTypeInfos(this.widgetsBundle.id.id).subscribe( + (widgets) => { + this.widgets = [...widgets]; + this.isDirty = false; + this.addMode = false; + this.editMode = !this.widgets.length; + this.cd.markForCheck(); + } + ); + } else { + this.addMode = false; + this.editMode = !this.widgets.length; + } + } + + save() { + const widgetTypeIds = this.widgets.map(w => w.id.id); + this.widgetsService.updateWidgetsBundleWidgetTypes(this.widgetsBundle.id.id, widgetTypeIds).subscribe(() => { + this.isDirty = false; + this.editMode = false; + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts index 6fd2804ae3..932c19f668 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts @@ -125,6 +125,10 @@ export class WidgetsBundlesTableConfigResolver implements Resolve { + this.openWidgetsBundle(null, widgetsBundle); + }; } resolve(): EntityTableConfig { diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.html b/ui-ngx/src/app/shared/components/breadcrumb.component.html index be7f1e6b16..594578465f 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.html +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.html @@ -37,11 +37,9 @@
- - - + {{ breadcrumb.icon }} - + {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : utils.customTranslation(breadcrumb.label, breadcrumb.label)) : (breadcrumb.label | translate) }} diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts index 2542f58871..380ddc59ce 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -117,7 +117,6 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { ignoreTranslate = false; } const icon = breadcrumbConfig.icon || 'home'; - const isMdiIcon = icon.startsWith('mdi:'); const link = [ route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/') ]; const breadcrumb = { id: guid(), @@ -125,7 +124,6 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { labelFunction, ignoreTranslate, icon, - isMdiIcon, link, queryParams: null }; diff --git a/ui-ngx/src/app/shared/components/breadcrumb.ts b/ui-ngx/src/app/shared/components/breadcrumb.ts index 599f8b85e8..77a832dbed 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.ts @@ -23,7 +23,6 @@ export interface BreadCrumb extends HasUUID{ labelFunction?: () => string; ignoreTranslate: boolean; icon: string; - isMdiIcon: boolean; link: any[]; queryParams: Params; } diff --git a/ui-ngx/src/app/shared/components/button/copy-button.component.scss b/ui-ngx/src/app/shared/components/button/copy-button.component.scss index 0818466a38..e26f6978cb 100644 --- a/ui-ngx/src/app/shared/components/button/copy-button.component.scss +++ b/ui-ngx/src/app/shared/components/button/copy-button.component.scss @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import "../../../../theme"; +@import "../../../../scss/constants"; :host { .mini-button { diff --git a/ui-ngx/src/app/shared/components/color-input.component.html b/ui-ngx/src/app/shared/components/color-input.component.html index c027104c6e..68ec5854f2 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.html +++ b/ui-ngx/src/app/shared/components/color-input.component.html @@ -37,12 +37,12 @@ diff --git a/ui-ngx/src/app/shared/components/color-input.component.scss b/ui-ngx/src/app/shared/components/color-input.component.scss index b81ac0fdf8..2660c16dc8 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.scss +++ b/ui-ngx/src/app/shared/components/color-input.component.scss @@ -32,10 +32,4 @@ margin: 0; } } - button.mat-mdc-button-base.color-box { - width: 40px; - min-width: 40px; - height: 40px; - padding: 7px; - } } diff --git a/ui-ngx/src/app/shared/components/color-input.component.ts b/ui-ngx/src/app/shared/components/color-input.component.ts index f22b91fde2..834fcb8176 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.ts +++ b/ui-ngx/src/app/shared/components/color-input.component.ts @@ -60,29 +60,13 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro @Input() requiredText: string; - private colorClearButtonValue: boolean; - get colorClearButton(): boolean { - return this.colorClearButtonValue; - } @Input() - set colorClearButton(value: boolean) { - const newVal = coerceBooleanProperty(value); - if (this.colorClearButtonValue !== newVal) { - this.colorClearButtonValue = newVal; - } - } + @coerceBoolean() + colorClearButton = false; - private openOnInputValue: boolean; - get openOnInput(): boolean { - return this.openOnInputValue; - } @Input() - set openOnInput(value: boolean) { - const newVal = coerceBooleanProperty(value); - if (this.openOnInputValue !== newVal) { - this.openOnInputValue = newVal; - } - } + @coerceBoolean() + openOnInput = false; private requiredValue: boolean; get required(): boolean { @@ -167,11 +151,12 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro showColorPicker($event: MouseEvent) { $event.stopPropagation(); - this.dialogs.colorPicker(this.colorFormGroup.get('color').value).subscribe( - (color) => { - if (color) { + this.dialogs.colorPicker(this.colorFormGroup.get('color').value, + this.colorClearButton).subscribe( + (result) => { + if (!result?.canceled) { this.colorFormGroup.patchValue( - {color}, {emitEvent: true} + {color: result?.color}, {emitEvent: true} ); this.cd.markForCheck(); } @@ -190,7 +175,8 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer, this.viewContainerRef, ColorPickerPanelComponent, 'left', true, null, { - color: this.colorFormGroup.get('color').value + color: this.colorFormGroup.get('color').value, + colorClearButton: this.colorClearButton }, {}, {}, {}, true); diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html index 56e4523b67..3ed6d4d14b 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html @@ -19,6 +19,14 @@
color.color
+
diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts index 219ed0ec56..38fcedf406 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts @@ -23,6 +23,12 @@ import { DialogComponent } from '@shared/components/dialog.component'; export interface ColorPickerDialogData { color: string; + colorClearButton: boolean; +} + +export interface ColorPickerDialogResult { + color?: string; + canceled?: boolean; } @Component({ @@ -30,24 +36,26 @@ export interface ColorPickerDialogData { templateUrl: './color-picker-dialog.component.html', styleUrls: ['./color-picker-dialog.component.scss'] }) -export class ColorPickerDialogComponent extends DialogComponent { +export class ColorPickerDialogComponent extends DialogComponent { color: string; + colorClearButton: boolean; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: ColorPickerDialogData, - public dialogRef: MatDialogRef) { + public dialogRef: MatDialogRef) { super(store, router, dialogRef); this.color = data.color; + this.colorClearButton = data.colorClearButton; } selectColor(color: string) { - this.dialogRef.close(color); + this.dialogRef.close({color}); } cancel(): void { - this.dialogRef.close(null); + this.dialogRef.close({canceled: true}); } } diff --git a/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html index 1051be6525..632e9216be 100644 --- a/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html @@ -23,6 +23,7 @@ close
diff --git a/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts index b6321c966d..53aec6d74f 100644 --- a/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts @@ -23,6 +23,12 @@ import { DialogComponent } from '@shared/components/dialog.component'; export interface MaterialIconsDialogData { icon: string; + iconClearButton: boolean; +} + +export interface MaterialIconsDialogResult { + icon?: string; + canceled?: boolean; } @Component({ @@ -31,24 +37,26 @@ export interface MaterialIconsDialogData { providers: [], styleUrls: ['./material-icons-dialog.component.scss'] }) -export class MaterialIconsDialogComponent extends DialogComponent { +export class MaterialIconsDialogComponent extends DialogComponent { selectedIcon: string; + iconClearButton: boolean; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData, - public dialogRef: MatDialogRef) { + public dialogRef: MatDialogRef) { super(store, router, dialogRef); this.selectedIcon = data.icon; + this.iconClearButton = data.iconClearButton; } selectIcon(icon: string) { - this.dialogRef.close(icon); + this.dialogRef.close({icon}); } cancel(): void { - this.dialogRef.close(null); + this.dialogRef.close({canceled: true}); } } diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts index ccc4a0a079..4874c6c186 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts @@ -122,20 +122,18 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte writeValue(value: EntityId | null): void { if (value != null) { - if (value.id === NULL_UUID) { - value.id = null; - } - this.modelValue = value; - this.entitySelectFormGroup.get('entityType').patchValue(value.entityType, {emitEvent: true}); - this.entitySelectFormGroup.get('entityId').patchValue(value, {emitEvent: true}); + this.modelValue = { + entityType: value.entityType, + id: value.id !== NULL_UUID ? value.id : null + }; } else { this.modelValue = { entityType: this.defaultEntityType, id: null }; - this.entitySelectFormGroup.get('entityType').patchValue(this.defaultEntityType, {emitEvent: true}); - this.entitySelectFormGroup.get('entityId').patchValue(null, {emitEvent: true}); } + this.entitySelectFormGroup.get('entityType').patchValue(this.modelValue.entityType, {emitEvent: false}); + this.entitySelectFormGroup.get('entityId').patchValue(this.modelValue.id, {emitEvent: false}); } updateView(entityType: EntityType | AliasEntityType | null, entityId: string | null) { diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html index 3ad2ef2824..44c61d605a 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html @@ -15,7 +15,9 @@ limitations under the License. --> - + {{ label }} - +
+ +
{{ subtypeListEmptyText | translate }} diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts index 1442b44cfd..cb7cc7b63d 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts @@ -15,22 +15,24 @@ /// import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; -import { Observable, Subscription, throwError } from 'rxjs'; -import { map, mergeMap, publishReplay, refCount, share } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; -import { AppState } from '@app/core/core.state'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Observable, ReplaySubject, Subscription, throwError } from 'rxjs'; +import { debounceTime, map, mergeMap, share } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { EntitySubtype, EntityType } from '@shared/models/entity-type.models'; import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; -import { MatChipInputEvent, MatChipGrid } from '@angular/material/chips'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { MatChipGrid, MatChipInputEvent } from '@angular/material/chips'; import { AssetService } from '@core/http/asset.service'; import { DeviceService } from '@core/http/device.service'; import { EdgeService } from '@core/http/edge.service'; import { EntityViewService } from '@core/http/entity-view.service'; import { BroadcastService } from '@core/services/broadcast.service'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { AlarmService } from '@core/http/alarm.service'; +import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; +import { coerceArray, coerceBoolean } from '@shared/decorators/coercion'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; @Component({ selector: 'tb-entity-subtype-list', @@ -46,39 +48,60 @@ import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; }) export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { - entitySubtypeListFormGroup: UntypedFormGroup; + entitySubtypeListFormGroup: FormGroup; modelValue: Array | null; private requiredValue: boolean; + get required(): boolean { return this.requiredValue; } - @Input() label: string; - @Input() + @coerceBoolean() set required(value: boolean) { - const newVal = coerceBooleanProperty(value); - if (this.requiredValue !== newVal) { - this.requiredValue = newVal; + if (this.requiredValue !== value) { + this.requiredValue = value; this.updateValidators(); } } + @Input() + floatLabel: FloatLabelType = 'auto'; + + @Input() + label: string; + @Input() disabled: boolean; @Input() entityType: EntityType; + @Input() + emptyInputPlaceholder: string; + + @Input() + filledInputPlaceholder: string; + + @Input() + appearance: MatFormFieldAppearance = 'fill'; + + @Input() + subscriptSizing: SubscriptSizing = 'fixed'; + + @Input() + @coerceArray() + additionalClasses: Array; + @ViewChild('entitySubtypeInput') entitySubtypeInput: ElementRef; @ViewChild('entitySubtypeAutocomplete') entitySubtypeAutocomplete: MatAutocomplete; @ViewChild('chipList', {static: true}) chipList: MatChipGrid; entitySubtypeList: Array = []; filteredEntitySubtypeList: Observable>; - entitySubtypes: Observable>; + private entitySubtypes: Observable>; private broadcastSubscription: Subscription; @@ -95,20 +118,25 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, private propagateChange = (v: any) => { }; - constructor(private store: Store, - private broadcast: BroadcastService, + private hasPageDataEntitySubTypes = new Set([ + EntityType.ALARM + ]); + + constructor(private broadcast: BroadcastService, public translate: TranslateService, private assetService: AssetService, private deviceService: DeviceService, private edgeService: EdgeService, private entityViewService: EntityViewService, - private fb: UntypedFormBuilder) { + private alarmService: AlarmService, + private fb: FormBuilder) { this.entitySubtypeListFormGroup = this.fb.group({ entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []], entitySubtype: [null] }); } + updateValidators() { this.entitySubtypeListFormGroup.get('entitySubtypeList').setValidators(this.required ? [Validators.required] : []); this.entitySubtypeListFormGroup.get('entitySubtypeList').updateValueAndValidity(); @@ -122,7 +150,6 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, } ngOnInit() { - switch (this.entityType) { case EntityType.ASSET: this.placeholder = this.required ? this.translate.instant('asset.enter-asset-type') @@ -164,14 +191,28 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, this.entitySubtypes = null; }); break; + case EntityType.ALARM: + this.placeholder = this.required ? this.translate.instant('alarm.enter-alarm-type') + : this.translate.instant('alarm.any-type'); + this.secondaryPlaceholder = '+' + this.translate.instant('alarm.alarm-type'); + this.noSubtypesMathingText = 'alarm.no-alarm-types-matching'; + this.subtypeListEmptyText = 'alarm.alarm-type-list-empty'; + break; + } + + if (this.emptyInputPlaceholder) { + this.placeholder = this.emptyInputPlaceholder; + } + if (this.filledInputPlaceholder) { + this.secondaryPlaceholder = this.filledInputPlaceholder; } - this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges - .pipe( - map(value => value ? value : ''), - mergeMap(name => this.fetchEntitySubtypes(name) ), - share() - ); + this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges.pipe( + debounceTime(150), + map(value => value ? value : ''), + mergeMap(name => this.fetchEntitySubtypes(name)), + share() + ); } ngAfterViewInit(): void { @@ -225,13 +266,6 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, } this.clear(''); } - - clearChipGrid() { - this.entitySubtypeList = []; - this.modelValue = null; - this.entitySubtypeListFormGroup.get('entitySubtypeList').patchValue([], {emitEvent: true}); - } - remove(entitySubtype: string) { const index = this.entitySubtypeList.indexOf(entitySubtype); if (index >= 0) { @@ -254,11 +288,16 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, return entitySubtype ? entitySubtype : undefined; } - fetchEntitySubtypes(searchText?: string): Observable> { + private fetchEntitySubtypes(searchText?: string): Observable> { this.searchText = searchText; - return this.getEntitySubtypes().pipe( + return this.getEntitySubtypes(searchText).pipe( map(subTypes => { - let result = subTypes.filter( subType => searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true); + let result; + if (this.hasPageDataEntitySubTypes.has(this.entityType)) { + result = subTypes; + } else { + result = subTypes.filter(subType => searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true); + } if (!result.length) { result = [searchText]; } @@ -267,7 +306,23 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, ); } - getEntitySubtypes(): Observable> { + private getEntitySubtypes(searchText?: string): Observable> { + if (this.hasPageDataEntitySubTypes.has(this.entityType)) { + const pageLink = new PageLink(25, 0, searchText); + let subTypesPagesObservable: Observable>; + switch (this.entityType) { + case EntityType.ALARM: + subTypesPagesObservable = this.alarmService.getAlarmTypes(pageLink, {ignoreLoading: true}); + break; + } + if (subTypesPagesObservable) { + this.entitySubtypes = subTypesPagesObservable.pipe( + map(subTypesPage => subTypesPage.data.map(subType => subType.type)), + ); + } else { + return throwError(null); + } + } if (!this.entitySubtypes) { let subTypesObservable: Observable>; switch (this.entityType) { @@ -287,8 +342,12 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, if (subTypesObservable) { this.entitySubtypes = subTypesObservable.pipe( map(subTypes => subTypes.map(subType => subType.type)), - publishReplay(1), - refCount() + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: true, + }), ); } else { return throwError(null); diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-type-list.component.html index 0503ce836b..936be07d18 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-type-list.component.html @@ -15,7 +15,11 @@ limitations under the License. --> - + {{ label }} +
+ +
- {{ 'entity.entity-type-list-empty' | translate }} + {{ 'entity.entity-type-list-required' | translate }}
diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts index 17f908b2e0..7bb52d0948 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts @@ -15,7 +15,7 @@ /// import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; @@ -25,9 +25,8 @@ import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/mod import { EntityService } from '@core/http/entity.service'; import { MatAutocomplete } from '@angular/material/autocomplete'; import { MatChipGrid } from '@angular/material/chips'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { FloatLabelType, SubscriptSizing } from '@angular/material/form-field'; -import { coerceBoolean } from '@shared/decorators/coercion'; +import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; +import { coerceArray, coerceBoolean } from '@shared/decorators/coercion'; interface EntityTypeInfo { name: string; @@ -48,7 +47,7 @@ interface EntityTypeInfo { }) export class EntityTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit { - entityTypeListFormGroup: UntypedFormGroup; + entityTypeListFormGroup: FormGroup; modelValue: Array | null; @@ -57,19 +56,28 @@ export class EntityTypeListComponent implements ControlValueAccessor, OnInit, Af return this.requiredValue; } - @Input() label: string; - - @Input() floatLabel: FloatLabelType = 'auto'; - @Input() + @coerceBoolean() set required(value: boolean) { - const newVal = coerceBooleanProperty(value); - if (this.requiredValue !== newVal) { - this.requiredValue = newVal; + if (this.requiredValue !== value) { + this.requiredValue = value; this.updateValidators(); } } + @Input() + @coerceArray() + additionalClasses: Array; + + @Input() + appearance: MatFormFieldAppearance = 'fill'; + + @Input() + label: string; + + @Input() + floatLabel: FloatLabelType = 'auto'; + @Input() disabled: boolean; @@ -79,6 +87,12 @@ export class EntityTypeListComponent implements ControlValueAccessor, OnInit, Af @Input() allowedEntityTypes: Array; + @Input() + emptyInputPlaceholder: string; + + @Input() + filledInputPlaceholder: string; + @Input() @coerceBoolean() ignoreAuthorityFilter: boolean; @@ -103,7 +117,7 @@ export class EntityTypeListComponent implements ControlValueAccessor, OnInit, Af constructor(private store: Store, public translate: TranslateService, private entityService: EntityService, - private fb: UntypedFormBuilder) { + private fb: FormBuilder) { this.entityTypeListFormGroup = this.fb.group({ entityTypeList: [this.entityTypeList, this.required ? [Validators.required] : []], entityType: [null] @@ -123,11 +137,17 @@ export class EntityTypeListComponent implements ControlValueAccessor, OnInit, Af } ngOnInit() { - - this.placeholder = this.required ? this.translate.instant('entity.enter-entity-type') - : this.translate.instant('entity.any-entity'); - this.secondaryPlaceholder = '+' + this.translate.instant('entity.entity-type'); - + if (this.emptyInputPlaceholder) { + this.placeholder = this.emptyInputPlaceholder; + } else { + this.placeholder = this.required ? this.translate.instant('entity.enter-entity-type') : + this.translate.instant('entity.any-entity'); + } + if (this.filledInputPlaceholder) { + this.secondaryPlaceholder = this.filledInputPlaceholder; + } else { + this.secondaryPlaceholder = '+' + this.translate.instant('entity.entity-type'); + } let entityTypes: Array; if (this.ignoreAuthorityFilter && this.allowedEntityTypes && this.allowedEntityTypes.length) { @@ -250,5 +270,4 @@ export class EntityTypeListComponent implements ControlValueAccessor, OnInit, Af this.entityTypeInput.nativeElement.focus(); }, 0); } - } diff --git a/ui-ngx/src/app/shared/components/fab-toolbar.component.scss b/ui-ngx/src/app/shared/components/fab-toolbar.component.scss index e8e0c0b9f2..42f2e0c9eb 100644 --- a/ui-ngx/src/app/shared/components/fab-toolbar.component.scss +++ b/ui-ngx/src/app/shared/components/fab-toolbar.component.scss @@ -74,7 +74,7 @@ mat-fab-toolbar { button.mat-mdc-fab { overflow: visible !important; opacity: .5; - mat-icon { + .mat-icon { position: relative; z-index: $z-index-fab + 2; opacity: 1; @@ -146,7 +146,7 @@ mat-fab-toolbar { box-shadow: none; opacity: 1; - mat-icon { + .mat-icon { opacity: 0; } } @@ -163,7 +163,7 @@ mat-fab-toolbar { mat-fab-trigger { button.mat-mdc-fab { transition: opacity .3s cubic-bezier(.55, 0, .55, .2) .2s; - mat-icon { + .mat-icon { transition: all $icon-delay ease-in; } } diff --git a/ui-ngx/src/app/shared/components/help-markdown.component.ts b/ui-ngx/src/app/shared/components/help-markdown.component.ts index 97bd326b46..90ac325c55 100644 --- a/ui-ngx/src/app/shared/components/help-markdown.component.ts +++ b/ui-ngx/src/app/shared/components/help-markdown.component.ts @@ -24,6 +24,7 @@ import { import { BehaviorSubject } from 'rxjs'; import { share } from 'rxjs/operators'; import { HelpService } from '@core/services/help.service'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-help-markdown', @@ -36,7 +37,9 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { @Input() helpContent: string; - @Input() visible: boolean; + @Input() + @coerceBoolean() + visible: boolean; @Input() style: { [klass: string]: any } = {}; diff --git a/ui-ngx/src/app/shared/components/help-popup.component.html b/ui-ngx/src/app/shared/components/help-popup.component.html index 1a9fe5541a..730d054b8b 100644 --- a/ui-ngx/src/app/shared/components/help-popup.component.html +++ b/ui-ngx/src/app/shared/components/help-popup.component.html @@ -30,19 +30,21 @@
-
+
diff --git a/ui-ngx/src/app/shared/components/help-popup.component.scss b/ui-ngx/src/app/shared/components/help-popup.component.scss index 2ec26a3c0f..6be4d9b71c 100644 --- a/ui-ngx/src/app/shared/components/help-popup.component.scss +++ b/ui-ngx/src/app/shared/components/help-popup.component.scss @@ -17,6 +17,9 @@ width: initial; display: inline-block; vertical-align: middle; + &.hint-button { + line-height: 1; + } } .tb-help-popup-button { @@ -65,4 +68,19 @@ vertical-align: middle; } } + &.hint-button { + padding: 2px 3px; + line-height: 1; + &.mat-mdc-outlined-button { + padding: 1px 2px; + } + .mdc-button__label > span { + .mat-icon { + margin-right: 0; + } + .mat-mdc-progress-spinner { + margin-right: 0; + } + } + } } diff --git a/ui-ngx/src/app/shared/components/help-popup.component.ts b/ui-ngx/src/app/shared/components/help-popup.component.ts index 08be61f728..7c698f52a5 100644 --- a/ui-ngx/src/app/shared/components/help-popup.component.ts +++ b/ui-ngx/src/app/shared/components/help-popup.component.ts @@ -28,9 +28,9 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { PopoverPlacement } from '@shared/components/popover.models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { isDefinedAndNotNull } from '@core/utils'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ - // eslint-disable-next-line @angular-eslint/component-selector selector: '[tb-help-popup], [tb-help-popup-content]', templateUrl: './help-popup.component.html', styleUrls: ['./help-popup.component.scss'], @@ -41,10 +41,8 @@ export class HelpPopupComponent implements OnChanges, OnDestroy { @ViewChild('toggleHelpButton', {read: ElementRef, static: false}) toggleHelpButton: ElementRef; @ViewChild('toggleHelpTextButton', {read: ElementRef, static: false}) toggleHelpTextButton: ElementRef; - // eslint-disable-next-line @angular-eslint/no-input-rename @Input('tb-help-popup') helpId: string; - // eslint-disable-next-line @angular-eslint/no-input-rename @Input('tb-help-popup-content') helpContent: string; // eslint-disable-next-line @angular-eslint/no-input-rename @@ -62,6 +60,11 @@ export class HelpPopupComponent implements OnChanges, OnDestroy { popoverVisible = false; popoverReady = true; + + @Input() + @coerceBoolean() + hintMode = false; + triggerSafeHtml: SafeHtml = null; textMode = false; diff --git a/ui-ngx/src/app/shared/components/help.component.ts b/ui-ngx/src/app/shared/components/help.component.ts index d7be6942dc..3ea2653658 100644 --- a/ui-ngx/src/app/shared/components/help.component.ts +++ b/ui-ngx/src/app/shared/components/help.component.ts @@ -18,13 +18,11 @@ import { Component, Input } from '@angular/core'; import { HelpLinks } from '@shared/models/constants'; @Component({ - // eslint-disable-next-line @angular-eslint/component-selector selector: '[tb-help]', templateUrl: './help.component.html' }) export class HelpComponent { - // eslint-disable-next-line @angular-eslint/no-input-rename @Input('tb-help') helpLinkId: string; gotoHelpPage(): void { diff --git a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html new file mode 100644 index 0000000000..7fff74a55e --- /dev/null +++ b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html @@ -0,0 +1,21 @@ + + +{{ hintIcon }} diff --git a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss new file mode 100644 index 0000000000..4febf04f15 --- /dev/null +++ b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 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 { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + .tb-hint-tooltip-icon { + color: #E0E0E0; + overflow: visible; + &:hover { + color: #9E9E9E; + } + } +} diff --git a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.ts b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.ts new file mode 100644 index 0000000000..2d6ee282c1 --- /dev/null +++ b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2023 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, Input } from '@angular/core'; +import { TooltipPosition } from '@angular/material/tooltip'; + +@Component({ + selector: '[tb-hint-tooltip-icon]', + templateUrl: './hint-tooltip-icon.component.html', + styleUrls: ['./hint-tooltip-icon.component.scss'] +}) +export class HintTooltipIconComponent { + + @Input('tb-hint-tooltip-icon') tooltipText: string; + + @Input() + tooltipPosition: TooltipPosition = 'right'; + + @Input() + hintIcon = 'info'; + +} diff --git a/ui-ngx/src/app/shared/components/icon.component.ts b/ui-ngx/src/app/shared/components/icon.component.ts new file mode 100644 index 0000000000..cf4359c369 --- /dev/null +++ b/ui-ngx/src/app/shared/components/icon.component.ts @@ -0,0 +1,281 @@ +/// +/// Copyright © 2016-2023 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 { CanColor, mixinColor } from '@angular/material/core'; +import { + AfterContentInit, + AfterViewChecked, + ChangeDetectionStrategy, + Component, + ElementRef, + ErrorHandler, + Inject, + OnDestroy, + Renderer2, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { MAT_ICON_LOCATION, MatIconLocation, MatIconRegistry } from '@angular/material/icon'; +import { Subscription } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; +import { ContentObserver } from '@angular/cdk/observers'; + +const _TbIconBase = mixinColor( + class { + constructor(public _elementRef: ElementRef) {} + }, +); + +const funcIriAttributes = [ + 'clip-path', + 'color-profile', + 'src', + 'cursor', + 'fill', + 'filter', + 'marker', + 'marker-start', + 'marker-mid', + 'marker-end', + 'mask', + 'stroke', +]; + +const funcIriAttributeSelector = funcIriAttributes.map(attr => `[${attr}]`).join(', '); + +const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/; + +@Component({ + template: '', + selector: 'tb-icon', + exportAs: 'tbIcon', + styleUrls: [], + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['color'], + // eslint-disable-next-line @angular-eslint/no-host-metadata-property + host: { + role: 'img', + class: 'mat-icon notranslate', + '[attr.data-mat-icon-type]': '!_useSvgIcon ? "font" : "svg"', + '[attr.data-mat-icon-name]': '_svgName', + '[attr.data-mat-icon-namespace]': '_svgNamespace', + '[class.mat-icon-no-color]': 'color !== "primary" && color !== "accent" && color !== "warn"', + }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TbIconComponent extends _TbIconBase + implements AfterContentInit, AfterViewChecked, CanColor, OnDestroy { + + @ViewChild('iconNameContent', {static: true}) + _iconNameContent: ElementRef; + + private icon: string; + + get viewValue(): string { + return (this._iconNameContent?.nativeElement.textContent || '').trim(); + } + + private _contentChanges = Subscription.EMPTY; + private _previousFontSetClass: string[] = []; + + _useSvgIcon = false; + _svgName: string | null; + _svgNamespace: string | null; + + private _textElement = null; + + private _previousPath?: string; + + private _elementsWithExternalReferences?: Map; + + private _currentIconFetch = Subscription.EMPTY; + + constructor(elementRef: ElementRef, + private contentObserver: ContentObserver, + private renderer: Renderer2, + private _iconRegistry: MatIconRegistry, + @Inject(MAT_ICON_LOCATION) private _location: MatIconLocation, + private readonly _errorHandler: ErrorHandler) { + super(elementRef); + } + + ngAfterContentInit(): void { + this.icon = this.viewValue; + this._updateIcon(); + this._contentChanges = this.contentObserver.observe(this._iconNameContent.nativeElement) + .subscribe(() => { + const content = this.viewValue; + if (this.icon !== content) { + this.icon = content; + this._updateIcon(); + } + }); + } + + ngAfterViewChecked() { + const cachedElements = this._elementsWithExternalReferences; + if (cachedElements && cachedElements.size) { + const newPath = this._location.getPathname(); + if (newPath !== this._previousPath) { + this._previousPath = newPath; + this._prependPathToReferences(newPath); + } + } + } + + ngOnDestroy() { + this._contentChanges.unsubscribe(); + this._currentIconFetch.unsubscribe(); + if (this._elementsWithExternalReferences) { + this._elementsWithExternalReferences.clear(); + } + } + + private _updateIcon() { + const useSvgIcon = isSvgIcon(this.icon); + if (this._useSvgIcon !== useSvgIcon) { + this._useSvgIcon = useSvgIcon; + if (!this._useSvgIcon) { + this._updateSvgIcon(undefined); + } else { + this._updateFontIcon(undefined); + } + } + if (this._useSvgIcon) { + this._updateSvgIcon(this.icon); + } else { + this._updateFontIcon(this.icon); + } + } + + private _updateFontIcon(rawName: string | undefined) { + if (rawName) { + this._clearFontIcon(); + const iconName = splitIconName(rawName)[1]; + this._textElement = this.renderer.createText(iconName); + const elem: HTMLElement = this._elementRef.nativeElement; + this.renderer.insertBefore(elem, this._textElement, this._iconNameContent.nativeElement); + const fontSetClasses = ( + this._iconRegistry.getDefaultFontSetClass() + ).filter(className => className.length > 0); + fontSetClasses.forEach(className => elem.classList.add(className)); + this._previousFontSetClass = fontSetClasses; + } else { + this._clearFontIcon(); + } + } + + private _clearFontIcon() { + const elem: HTMLElement = this._elementRef.nativeElement; + if (this._textElement !== null) { + this.renderer.removeChild(elem, this._textElement); + this._textElement = null; + } + this._previousFontSetClass.forEach(className => elem.classList.remove(className)); + this._previousFontSetClass = []; + } + + private _updateSvgIcon(rawName: string | undefined) { + this._svgNamespace = null; + this._svgName = null; + this._currentIconFetch.unsubscribe(); + + if (rawName) { + const [namespace, iconName] = splitIconName(rawName); + if (namespace) { + this._svgNamespace = namespace; + } + if (iconName) { + this._svgName = iconName; + } + this._iconRegistry.getDefaultFontSetClass(); + this._currentIconFetch = this._iconRegistry + .getNamedSvgIcon(iconName, namespace) + .pipe(take(1)) + .subscribe({ + next: (svg) => this._setSvgElement(svg), + error: (err: Error) => { + const errorMessage = `Error retrieving icon ${namespace}:${iconName}! ${err.message}`; + this._errorHandler.handleError(new Error(errorMessage)); + } + }); + } else { + this._clearSvgElement(); + } + } + + private _setSvgElement(svg: SVGElement) { + this._clearSvgElement(); + const path = this._location.getPathname(); + this._previousPath = path; + this._cacheChildrenWithExternalReferences(svg); + this._prependPathToReferences(path); + this.renderer.insertBefore(this._elementRef.nativeElement, svg, this._iconNameContent.nativeElement); + } + + private _clearSvgElement() { + const layoutElement: HTMLElement = this._elementRef.nativeElement; + let childCount = layoutElement.childNodes.length; + if (this._elementsWithExternalReferences) { + this._elementsWithExternalReferences.clear(); + } + while (childCount--) { + const child = layoutElement.childNodes[childCount]; + if (child.nodeType !== 1 || child.nodeName.toLowerCase() === 'svg') { + child.remove(); + } + } + } + + private _cacheChildrenWithExternalReferences(element: SVGElement) { + const elementsWithFuncIri = element.querySelectorAll(funcIriAttributeSelector); + const elements = (this._elementsWithExternalReferences = this._elementsWithExternalReferences || new Map()); + elementsWithFuncIri.forEach( + (elementWithFuncIri) => { + funcIriAttributes.forEach(attr => { + const elementWithReference = elementWithFuncIri; + const value = elementWithReference.getAttribute(attr); + const match = value ? value.match(funcIriPattern) : null; + + if (match) { + let attributes = elements.get(elementWithReference); + + if (!attributes) { + attributes = []; + elements.set(elementWithReference, attributes); + } + + attributes.push({name: attr, value: match[1]}); + } + }); + } + ); + } + + private _prependPathToReferences(path: string) { + const elements = this._elementsWithExternalReferences; + if (elements) { + elements.forEach((attrs, element) => { + attrs.forEach(attr => { + element.setAttribute(attr.name, `url('${path}#${attr.value}')`); + }); + }); + } + } + +} 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 d2512a8148..2ed771d8b2 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.html +++ b/ui-ngx/src/app/shared/components/image-input.component.html @@ -20,38 +20,14 @@
-
-
-
-
{{ (disabled ? 'dashboard.empty-image' : 'dashboard.no-image') | translate }}
- -
-
- -
-
-
-
-
- cloud_upload - image-input.drop-image-or - - +
+
+
{{ 'dashboard.empty-image' | translate }}
+
-
-
-
-
+
+
+ cloud_upload +
+ image-input.drag-and-drop +
+ image-input.or + + +
+
+
+
+ +
dashboard.maximum-upload-file-size
diff --git a/ui-ngx/src/app/shared/components/image-input.component.scss b/ui-ngx/src/app/shared/components/image-input.component.scss index a776bc8cd7..b7d05bef8d 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.scss +++ b/ui-ngx/src/app/shared/components/image-input.component.scss @@ -15,9 +15,8 @@ */ @import "../../../scss/constants"; -$containerHeight: 120px !default; -$previewContainerWidth: 168px !default; -$previewSize: 96px !default; +$containerHeight: 96px !default; +$previewSize: 78px !default; :host { @@ -31,30 +30,39 @@ $previewSize: 96px !default; } .tb-image-select-container { - position: relative; width: 100%; height: $containerHeight; + display: flex; + gap: 12px; + align-items: center; } .image-container { - position: relative; - float: left; - height: $containerHeight; - padding: 12px; - margin-right: 8px; - background: rgba(0, 0, 0, 0.03); + background: #F3F6FA; border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.05); + padding: 8px 12px 8px 8px; + display: flex; + align-items: center; + gap: 12px; + &.disabled { + padding: 8px; + } } - .image-content-container { - background: #FFFFFF; - border: 1px solid rgba(0, 0, 0, 0.2); - border-radius: 4px; - padding-left: 8px; + .tb-image-preview-container { + width: $previewSize; height: $previewSize; - &.no-padding { - padding-left: 0px; - } + border: 1px solid rgba(0, 0, 0, 0.12); + background: #fff; + display: flex; + align-items: center; + justify-content: center; + } + + .tb-image-preview-text { + font-size: 14px; + text-align: center; } .tb-image-preview { @@ -64,57 +72,18 @@ $previewSize: 96px !default; max-height: $previewSize - 2px; } - .tb-image-preview-container { - position: relative; - float: left; - width: $previewSize; - height: $previewSize; - margin-top: -1px; - margin-bottom: -1px; - border: 1px solid rgba(0, 0, 0, 0.54); - - div { - width: 100%; - font-size: 18px; - text-align: center; - } - - div, - .tb-image-preview { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - } - - .tb-image-clear-container { - position: relative; - float: right; - height: $previewSize; - display: flex; - align-items: center; - &.full-height { - height: $containerHeight; - } - } - .file-input { display: none; } .tb-flow-drop { - position: relative; - height: $containerHeight; + height: 100%; + flex: 1; overflow: hidden; border: 2px dashed rgba(0, 0, 0, 0.2); border-radius: 4px; box-sizing: border-box; - &.float-left { - float: left; - } - .upload-label { width: 100%; height: 100%; @@ -123,11 +92,29 @@ $previewSize: 96px !default; flex-direction: row; justify-content: center; align-items: center; - font-size: 16px; - color: rgba(0, 0, 0, 0.54); - text-align: center; + gap: 8px; .mat-icon { - margin-right: 17px; + color: rgba(0,0,0,0.12); + } + .upload-text-area { + display: flex; + flex-direction: column; + align-items: center; + font-size: 16px; + line-height: 24px; + color: rgba(0, 0, 0, 0.54); + text-align: center; + .hide-xs { + @media #{$mat-xs} { + display: none; + } + } + } + .upload-button-area { + display: flex; + justify-content: center; + align-items: flex-start; + gap: 6px; } } } @@ -138,13 +125,14 @@ $previewSize: 96px !default; } :host ::ng-deep { - button.browse-file { + button.mat-mdc-button.mat-mdc-button-base.browse-file { padding: 0; + min-width: 0; + height: 24px; font-size: 16px; label { display: block; cursor: pointer; - padding: 0 16px; } } } diff --git a/ui-ngx/src/app/shared/components/json-form/json-form.component.ts b/ui-ngx/src/app/shared/components/json-form/json-form.component.ts index 6ed1d133ae..6da145d826 100644 --- a/ui-ngx/src/app/shared/components/json-form/json-form.component.ts +++ b/ui-ngx/src/app/shared/components/json-form/json-form.component.ts @@ -216,9 +216,9 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato private onColorClick(key: (string | number)[], val: tinycolor.ColorFormats.RGBA, colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) { - this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((color) => { - if (color && colorSelectedFn) { - colorSelectedFn(tinycolor(color).toRgb()); + this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((result) => { + if (!result?.canceled && colorSelectedFn) { + colorSelectedFn(tinycolor(result?.color).toRgb()); } }); } @@ -226,9 +226,9 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato private onIconClick(key: (string | number)[], val: string, iconSelectedFn: (icon: string) => void) { - this.dialogs.materialIconPicker(val).subscribe((icon) => { - if (icon && iconSelectedFn) { - iconSelectedFn(icon); + this.dialogs.materialIconPicker(val).subscribe((result) => { + if (!result?.canceled && iconSelectedFn) { + iconSelectedFn(result?.icon); } }); } diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index cd03959fad..7936d2f51a 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -17,7 +17,6 @@ import { ChangeDetectorRef, Component, - ComponentFactory, ComponentRef, ElementRef, EventEmitter, @@ -91,7 +90,7 @@ export class TbMarkdownComponent implements OnChanges { error = null; private tbMarkdownInstanceComponentRef: ComponentRef; - private tbMarkdownInstanceComponentFactory: ComponentFactory; + private tbMarkdownInstanceComponentType: Type; constructor(private help: HelpService, private cd: ChangeDetectorRef, @@ -153,7 +152,7 @@ export class TbMarkdownComponent implements OnChanges { if (this.additionalCompileModules) { compileModules = compileModules.concat(this.additionalCompileModules); } - this.dynamicComponentFactoryService.createDynamicComponentFactory( + this.dynamicComponentFactoryService.createDynamicComponent( class TbMarkdownInstance { ngOnDestroy(): void { parent.destroyMarkdownInstanceResources(); @@ -162,12 +161,13 @@ export class TbMarkdownComponent implements OnChanges { template, compileModules, true, 1, styles - ).subscribe((factory) => { - this.tbMarkdownInstanceComponentFactory = factory; + ).subscribe((componentData) => { + this.tbMarkdownInstanceComponentType = componentData.componentType; const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); try { this.tbMarkdownInstanceComponentRef = - this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector); + this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentType, + {index: 0, injector, ngModuleRef: componentData.componentModuleRef}); if (this.context) { for (const propName of Object.keys(this.context)) { this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName]; @@ -261,9 +261,9 @@ export class TbMarkdownComponent implements OnChanges { } private destroyMarkdownInstanceResources() { - if (this.tbMarkdownInstanceComponentFactory) { - this.dynamicComponentFactoryService.destroyDynamicComponentFactory(this.tbMarkdownInstanceComponentFactory); - this.tbMarkdownInstanceComponentFactory = null; + if (this.tbMarkdownInstanceComponentType) { + this.dynamicComponentFactoryService.destroyDynamicComponent(this.tbMarkdownInstanceComponentType); + this.tbMarkdownInstanceComponentType = null; } this.tbMarkdownInstanceComponentRef = null; } diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.html b/ui-ngx/src/app/shared/components/material-icon-select.component.html index 8bae8c3435..8f7aa303f9 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.html +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.html @@ -16,7 +16,7 @@ -->
- {{materialIconFormGroup.get('icon').value}} + {{materialIconFormGroup.get('icon').value}} {{ label }} @@ -24,18 +24,18 @@ type="button" matSuffix mat-icon-button aria-label="Clear" (click)="clear()"> - close + close
diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.scss b/ui-ngx/src/app/shared/components/material-icon-select.component.scss index b008fc6838..78e4d235cf 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.scss +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.scss @@ -24,21 +24,3 @@ } } } - -:host ::ng-deep { - button.mat-mdc-button-base.icon-box { - width: 40px; - min-width: 40px; - height: 40px; - padding: 7px; - &:not(:disabled) { - color: rgba(0, 0, 0, 0.87); - } - > .mat-icon { - width: 24px; - height: 24px; - font-size: 24px; - margin: 0; - } - } -} diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts index 740bb4545a..255aeb0668 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -54,17 +54,9 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit @Input() disabled: boolean; - private iconClearButtonValue: boolean; - get iconClearButton(): boolean { - return this.iconClearButtonValue; - } @Input() - set iconClearButton(value: boolean) { - const newVal = coerceBooleanProperty(value); - if (this.iconClearButtonValue !== newVal) { - this.iconClearButtonValue = newVal; - } - } + @coerceBoolean() + iconClearButton = false; private requiredValue: boolean; get required(): boolean { @@ -135,11 +127,12 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit openIconDialog() { if (!this.disabled) { - this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value).subscribe( - (icon) => { - if (icon) { + this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value, + this.iconClearButton).subscribe( + (result) => { + if (!result?.canceled) { this.materialIconFormGroup.patchValue( - {icon}, {emitEvent: true} + {icon: result?.icon}, {emitEvent: true} ); this.cd.markForCheck(); } @@ -159,7 +152,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit const materialIconsPopover = this.popoverService.displayPopover(trigger, this.renderer, this.viewContainerRef, MaterialIconsComponent, 'left', true, null, { - selectedIcon: this.materialIconFormGroup.get('icon').value + selectedIcon: this.materialIconFormGroup.get('icon').value, + iconClearButton: this.iconClearButton }, {}, {}, {}, true); diff --git a/ui-ngx/src/app/shared/components/material-icons.component.html b/ui-ngx/src/app/shared/components/material-icons.component.html index 39404a9998..bbc50a9292 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.html +++ b/ui-ngx/src/app/shared/components/material-icons.component.html @@ -39,7 +39,7 @@ matTooltip="{{ icon.displayName }}" matTooltipPosition="above" type="button"> - {{icon.name}} + {{icon.name}}
-
{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}
+
+ + + +
diff --git a/ui-ngx/src/app/shared/components/material-icons.component.scss b/ui-ngx/src/app/shared/components/material-icons.component.scss index 23b959d118..b9c7a74c9a 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.scss +++ b/ui-ngx/src/app/shared/components/material-icons.component.scss @@ -26,7 +26,7 @@ letter-spacing: 0.25px; color: rgba(0, 0, 0, 0.87); } - .tb-material-icons-title, .tb-material-icons-search, .tb-material-icons-show-more { + .tb-material-icons-title, .tb-material-icons-search { width: 100%; } .tb-material-icons-viewport { @@ -58,4 +58,12 @@ margin: 0; } } + .tb-material-icons-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + align-items: flex-start; + align-self: stretch; + } } diff --git a/ui-ngx/src/app/shared/components/material-icons.component.ts b/ui-ngx/src/app/shared/components/material-icons.component.ts index 9347243af2..1d826816b7 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.ts +++ b/ui-ngx/src/app/shared/components/material-icons.component.ts @@ -36,6 +36,7 @@ import { ResourcesService } from '@core/services/resources.service'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { BreakpointObserver } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-material-icons', @@ -52,6 +53,10 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { @Input() selectedIcon: string; + @Input() + @coerceBoolean() + iconClearButton = false; + @Input() popover: TbPopoverComponent; @@ -123,6 +128,10 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { this.iconSelected.emit(icon.name); } + clearIcon() { + this.iconSelected.emit(null); + } + private calculatePanelSize(iconsRowSize: number, iconRows = 4) { this.iconsPanelHeight = Math.min(iconRows * this.iconsRowHeight, 10 * this.iconsRowHeight) + 'px'; this.iconsPanelWidth = (iconsRowSize * 36 + (iconsRowSize - 1) * 12 + 6) + 'px'; diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.html b/ui-ngx/src/app/shared/components/notification/notification.component.html index b9ce3a840f..ba6091eebc 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.html +++ b/ui-ngx/src/app/shared/components/notification/notification.component.html @@ -18,14 +18,14 @@
- + {{ notification.additionalConfig.icon.icon }} - +
- + {{ notificationTypeIcons.get(notification.type) }} - +
diff --git a/ui-ngx/src/app/shared/components/phone-input.component.scss b/ui-ngx/src/app/shared/components/phone-input.component.scss index eb8d27646f..1ba5eabf3a 100644 --- a/ui-ngx/src/app/shared/components/phone-input.component.scss +++ b/ui-ngx/src/app/shared/components/phone-input.component.scss @@ -24,7 +24,6 @@ .phone-input-container { display: flex; - align-items: center; .phone-input { width: 100%; @@ -32,10 +31,11 @@ } .flags-select-container { - display: inline-block; + display: flex; + align-items: center; position: relative; width: 50px; - height: 100%; + height: 56px; margin-right: 5px; } diff --git a/ui-ngx/src/app/shared/components/popover.component.ts b/ui-ngx/src/app/shared/components/popover.component.ts index 91f5a2e902..3b9c56574d 100644 --- a/ui-ngx/src/app/shared/components/popover.component.ts +++ b/ui-ngx/src/app/shared/components/popover.component.ts @@ -52,9 +52,9 @@ import { getPlacementName, popoverMotion, PopoverPlacement, - POSITION_MAP, PropertyMapping } from '@shared/components/popover.models'; +import { POSITION_MAP } from '@shared/models/overlay.models'; import { distinctUntilChanged, take, takeUntil } from 'rxjs/operators'; import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils'; import { animate, AnimationBuilder, AnimationMetadata, style } from '@angular/animations'; diff --git a/ui-ngx/src/app/shared/components/popover.models.ts b/ui-ngx/src/app/shared/components/popover.models.ts index b9ca5e9d59..58a631673e 100644 --- a/ui-ngx/src/app/shared/components/popover.models.ts +++ b/ui-ngx/src/app/shared/components/popover.models.ts @@ -15,8 +15,9 @@ /// import { animate, AnimationTriggerMetadata, style, transition, trigger } from '@angular/animations'; -import { ConnectedOverlayPositionChange, ConnectionPositionPair } from '@angular/cdk/overlay'; +import { ConnectedOverlayPositionChange } from '@angular/cdk/overlay'; import { TbPopoverComponent } from '@shared/components/popover.component'; +import { POSITION_MAP } from '@shared/models/overlay.models'; export const popoverMotion: AnimationTriggerMetadata = trigger('popoverMotion', [ transition('void => active', [ @@ -45,30 +46,6 @@ export const PopoverPlacements = ['top', 'topLeft', 'topRight', 'right', 'rightT type PopoverPlacementTuple = typeof PopoverPlacements; export type PopoverPlacement = PopoverPlacementTuple[number]; -export const POSITION_MAP: { [key: string]: ConnectionPositionPair } = { - top: new ConnectionPositionPair({ originX: 'center', originY: 'top' }, { overlayX: 'center', overlayY: 'bottom' }), - topLeft: new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'bottom' }), - topRight: new ConnectionPositionPair({ originX: 'end', originY: 'top' }, { overlayX: 'end', overlayY: 'bottom' }), - right: new ConnectionPositionPair({ originX: 'end', originY: 'center' }, { overlayX: 'start', overlayY: 'center' }), - rightTop: new ConnectionPositionPair({ originX: 'end', originY: 'top' }, { overlayX: 'start', overlayY: 'top' }), - rightBottom: new ConnectionPositionPair( - { originX: 'end', originY: 'bottom' }, - { overlayX: 'start', overlayY: 'bottom' } - ), - bottom: new ConnectionPositionPair({ originX: 'center', originY: 'bottom' }, { overlayX: 'center', overlayY: 'top' }), - bottomLeft: new ConnectionPositionPair( - { originX: 'start', originY: 'bottom' }, - { overlayX: 'start', overlayY: 'top' } - ), - bottomRight: new ConnectionPositionPair({ originX: 'end', originY: 'bottom' }, { overlayX: 'end', overlayY: 'top' }), - left: new ConnectionPositionPair({ originX: 'start', originY: 'center' }, { overlayX: 'end', overlayY: 'center' }), - leftTop: new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'end', overlayY: 'top' }), - leftBottom: new ConnectionPositionPair( - { originX: 'start', originY: 'bottom' }, - { overlayX: 'end', overlayY: 'bottom' } - ) -}; - export const DEFAULT_POPOVER_POSITIONS = [POSITION_MAP.top, POSITION_MAP.right, POSITION_MAP.bottom, POSITION_MAP.left]; export function getPlacementName(position: ConnectedOverlayPositionChange): PopoverPlacement | undefined { diff --git a/ui-ngx/src/app/shared/components/public-api.ts b/ui-ngx/src/app/shared/components/public-api.ts index 04508266e9..4647fc6bad 100644 --- a/ui-ngx/src/app/shared/components/public-api.ts +++ b/ui-ngx/src/app/shared/components/public-api.ts @@ -27,3 +27,5 @@ export * from './toggle-header.component'; export * from './toggle-select.component'; export * from './unit-input.component'; export * from './material-icons.component'; +export * from './icon.component'; +export * from './hint-tooltip-icon.component'; diff --git a/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html index c057f1df09..fd15936a45 100644 --- a/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html @@ -15,8 +15,9 @@ limitations under the License. --> - - {{ label }} + + {{ 'relation.relation-type' | translate }} ; - @Input() floatLabel: FloatLabelType = 'auto'; + @Input() + appearance: MatFormFieldAppearance = 'fill'; @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } + @coerceBoolean() + required: boolean; @Input() disabled: boolean; @@ -75,7 +76,7 @@ export class RelationTypeAutocompleteComponent implements ControlValueAccessor, constructor(private store: Store, private broadcast: BroadcastService, public translate: TranslateService, - private fb: UntypedFormBuilder) { + private fb: FormBuilder) { this.relationTypeFormGroup = this.fb.group({ relationType: [null, this.required ? [Validators.required, Validators.maxLength(255)] : [Validators.maxLength(255)]] }); diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.html b/ui-ngx/src/app/shared/components/string-items-list.component.html index 4677cbc3de..7416e96870 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.html +++ b/ui-ngx/src/app/shared/components/string-items-list.component.html @@ -54,7 +54,15 @@ {{ 'common.not-found' | translate }} - {{ hint }} + + {{ hint }} + + + + +
+ +
{{ requiredText }} diff --git a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss index 6f38e6a6a5..f6f24e2608 100644 --- a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss +++ b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss @@ -51,7 +51,7 @@ margin: 2px; line-height: 24px; - mat-icon { + .mat-icon { width: 24px; height: 24px; @@ -93,7 +93,7 @@ margin: 0; line-height: 28px; - mat-icon { + .mat-icon { width: 24px; height: 24px; font-size: 24px; diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss index ff3a9c5438..74af632802 100644 --- a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss +++ b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss @@ -17,6 +17,7 @@ :host { min-width: 355px; + display: block; @media #{$mat-xs} { min-width: 0; diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.html b/ui-ngx/src/app/shared/components/time/timewindow.component.html index e445e99c98..a53ea656fc 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.html @@ -41,26 +41,16 @@
- - + [class]="{'no-padding': noPadding}" + matTooltip="{{ 'timewindow.edit' | translate }}" + [matTooltipPosition]="tooltipPosition" + [style]="timewindowComponentStyle" + (click)="toggleTimewindow($event)"> + {{ computedTimewindowStyle.icon }} +
{{innerValue?.displayValue}} | {{innerValue?.displayTimezoneAbbr}} - - +
+ {{ computedTimewindowStyle.icon }}
diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.scss b/ui-ngx/src/app/shared/components/time/timewindow.component.scss index 695362197d..a00fe5bdc1 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.scss +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.scss @@ -17,19 +17,31 @@ min-width: 48px; margin: 8px 0; max-width: 100%; + &.no-margin { + margin: 0; + } .mdc-button { max-width: 100%; } section.tb-timewindow { - min-height: 32px; padding: 0 8px; + &.no-padding { + padding: 0; + } + line-height: 32px; + pointer-events: all; + cursor: pointer; + display: flex; + flex-direction: row; + place-content: center flex-start; + align-items: center; + gap: 4px; + width: 100%; - span { + .tb-timewindow-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - pointer-events: all; - cursor: pointer; } .timezone-abbr { diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts index f9b40daac1..bd19cf0c2a 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts @@ -19,8 +19,12 @@ import { Component, ElementRef, forwardRef, + HostBinding, Injector, Input, + OnChanges, + OnInit, + SimpleChanges, StaticProvider, ViewContainerRef } from '@angular/core'; @@ -47,9 +51,18 @@ import { TimeService } from '@core/services/time.service'; import { TooltipPosition } from '@angular/material/tooltip'; import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { ComponentPortal } from '@angular/cdk/portal'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { + ComponentStyle, + defaultTimewindowStyle, + iconStyle, + textStyle, + TimewindowStyle +} from '@shared/models/widget-settings.models'; +import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models'; +import { fromEvent } from 'rxjs'; // @dynamic @Component({ @@ -64,7 +77,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; } ] }) -export class TimewindowComponent implements ControlValueAccessor { +export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChanges { historyOnlyValue = false; @@ -83,6 +96,24 @@ export class TimewindowComponent implements ControlValueAccessor { return this.historyOnlyValue; } + get displayTypePrefix(): boolean { + return isDefinedAndNotNull(this.computedTimewindowStyle?.displayTypePrefix) + ? this.computedTimewindowStyle?.displayTypePrefix : true; + } + + @HostBinding('class.no-margin') + @Input() + @coerceBoolean() + noMargin = false; + + @Input() + @coerceBoolean() + noPadding = false; + + @Input() + @coerceBoolean() + disablePanel = false; + @Input() @coerceBoolean() forAllTimeEnabled = false; @@ -140,10 +171,10 @@ export class TimewindowComponent implements ControlValueAccessor { } @Input() - direction: 'left' | 'right' = 'left'; + tooltipPosition: TooltipPosition = 'above'; @Input() - tooltipPosition: TooltipPosition = 'above'; + timewindowStyle: TimewindowStyle; @Input() @coerceBoolean() @@ -153,6 +184,10 @@ export class TimewindowComponent implements ControlValueAccessor { timewindowDisabled: boolean; + computedTimewindowStyle: TimewindowStyle; + timewindowComponentStyle: ComponentStyle; + timewindowIconStyle: ComponentStyle; + private propagateChange = (_: any) => {}; constructor(private overlay: Overlay, @@ -165,10 +200,29 @@ export class TimewindowComponent implements ControlValueAccessor { public viewContainerRef: ViewContainerRef) { } + ngOnInit() { + this.updateTimewindowStyle(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'timewindowStyle') { + this.updateTimewindowStyle(); + this.updateDisplayValue(); + } + } + } + } + toggleTimewindow($event: Event) { if ($event) { $event.stopPropagation(); } + if (this.disablePanel) { + return; + } const config = new OverlayConfig({ panelClass: 'tb-timewindow-panel', backdropClass: 'cdk-overlay-transparent-backdrop', @@ -176,14 +230,10 @@ export class TimewindowComponent implements ControlValueAccessor { maxHeight: '80vh', height: 'min-content' }); - const connectedPosition: ConnectedPosition = { - originX: 'start', - originY: 'bottom', - overlayX: 'start', - overlayY: 'top' - }; - config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement) - .withPositions([connectedPosition]); + + config.positionStrategy = this.overlay.position() + .flexibleConnectedTo(this.nativeElement) + .withPositions(DEFAULT_OVERLAY_POSITIONS); const overlayRef = this.overlay.create(config); overlayRef.backdropClick().subscribe(() => { @@ -210,7 +260,11 @@ export class TimewindowComponent implements ControlValueAccessor { const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); const componentRef = overlayRef.attach(new ComponentPortal(TimewindowPanelComponent, this.viewContainerRef, injector)); + const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => { + overlayRef.updatePosition(); + }); componentRef.onDestroy(() => { + resizeWindows$.unsubscribe(); if (componentRef.instance.result) { this.innerValue = componentRef.instance.result; this.timewindowDisabled = this.isTimewindowDisabled(); @@ -221,6 +275,17 @@ export class TimewindowComponent implements ControlValueAccessor { this.cd.detectChanges(); } + private updateTimewindowStyle() { + if (!this.asButton) { + this.computedTimewindowStyle = {...defaultTimewindowStyle, ...(this.timewindowStyle || {})}; + this.timewindowComponentStyle = textStyle(this.computedTimewindowStyle.font); + if (this.computedTimewindowStyle.color) { + this.timewindowComponentStyle.color = this.computedTimewindowStyle.color; + } + this.timewindowIconStyle = this.computedTimewindowStyle.iconSize ? iconStyle(this.computedTimewindowStyle.iconSize) : {}; + } + } + private onHistoryOnlyChanged(): boolean { if (this.historyOnlyValue && this.innerValue && this.innerValue.selectedTab !== TimewindowType.HISTORY) { this.innerValue.selectedTab = TimewindowType.HISTORY; @@ -264,7 +329,7 @@ export class TimewindowComponent implements ControlValueAccessor { updateDisplayValue() { if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) { - this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - '; + this.innerValue.displayValue = this.displayTypePrefix ? (this.translate.instant('timewindow.realtime') + ' - ') : ''; if (this.innerValue.realtime.realtimeType === RealtimeWindowType.INTERVAL) { this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.realtime.quickInterval)); } else { @@ -272,7 +337,7 @@ export class TimewindowComponent implements ControlValueAccessor { this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs); } } else { - this.innerValue.displayValue = (!this.historyOnly || this.alwaysDisplayTypePrefix) ? + this.innerValue.displayValue = this.displayTypePrefix && (!this.historyOnly || this.alwaysDisplayTypePrefix) ? (this.translate.instant('timewindow.history') + ' - ') : ''; if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.html b/ui-ngx/src/app/shared/components/toggle-header.component.html index d7ed76de90..7aa391b3b4 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.html +++ b/ui-ngx/src/app/shared/components/toggle-header.component.html @@ -15,14 +15,33 @@ limitations under the License. --> - - {{ option.name }} - + +
+ + {{ option.name }} + +
+ diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.scss b/ui-ngx/src/app/shared/components/toggle-header.component.scss index 6a6785c11b..7091c68c93 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.scss +++ b/ui-ngx/src/app/shared/components/toggle-header.component.scss @@ -14,11 +14,38 @@ * limitations under the License. */ -@import "../../../theme"; @import "../../../scss/constants"; +:host { + max-width: 100%; + display: grid; + grid-template-columns: min-content minmax(auto, 1fr) min-content; + .tb-toggle-header-pagination-button { + display: none; + } + &.tb-toggle-header-pagination-controls-enabled { + .tb-toggle-header-pagination-button { + display: block; + } + } + .tb-toggle-container, .tb-toggle-header-select { + display: inline-grid; + grid-column: 2; + overflow: hidden; + } + .tb-toggle-container { + &.tb-disable-pagination { + overflow: visible; + } + } + .tb-toggle-header { + transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1); + } +} + :host ::ng-deep { .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { + overflow: visible; width: 100%; border-radius: 100px; height: 32px; @@ -152,9 +179,6 @@ line-height: 16px; letter-spacing: 0.25px; } - .mat-mdc-select-value { - color: rgba(0, 0, 0, 0.38); - } .mat-mdc-select-arrow-wrapper { height: 12px; padding-left: 6px; diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.ts b/ui-ngx/src/app/shared/components/toggle-header.component.ts index 35daad0e3f..5781f9fac9 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.ts +++ b/ui-ngx/src/app/shared/components/toggle-header.component.ts @@ -15,27 +15,33 @@ /// import { - AfterContentInit, + AfterContentChecked, + AfterContentInit, AfterViewChecked, + AfterViewInit, ChangeDetectorRef, Component, ContentChildren, Directive, ElementRef, EventEmitter, + HostBinding, Input, OnDestroy, OnInit, Output, - QueryList + QueryList, + ViewChild } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { Subject, Subscription } from 'rxjs'; +import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { coerceBoolean } from '@shared/decorators/coercion'; import { startWith, takeUntil } from 'rxjs/operators'; +import { Platform } from '@angular/cdk/platform'; +import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle'; export interface ToggleHeaderOption { name: string; @@ -44,6 +50,8 @@ export interface ToggleHeaderOption { export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked'; +export type ScrollDirection = 'after' | 'before'; + @Directive( { // eslint-disable-next-line @angular-eslint/directive-selector @@ -72,7 +80,7 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI @Input() options: ToggleHeaderOption[] = []; - private _destroyed = new Subject(); + protected _destroyed = new Subject(); protected constructor(protected store: Store) { super(store); @@ -109,7 +117,36 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI templateUrl: './toggle-header.component.html', styleUrls: ['./toggle-header.component.scss'] }) -export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterContentInit, OnDestroy { +export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterViewInit, AfterContentInit, + AfterContentChecked, AfterViewChecked, OnDestroy { + + @ViewChild('toggleGroup', {static: false}) + toggleGroup: ElementRef; + + @ViewChild(MatButtonToggleGroup, {static: false}) + buttonToggleGroup: MatButtonToggleGroup; + + @ViewChild('toggleGroupContainer', {static: false}) + toggleGroupContainer: ElementRef; + + @HostBinding('class.tb-toggle-header-pagination-controls-enabled') + private showPaginationControls = false; + private _showPaginationControlsChanged = false; + + private toggleGroupResize$: ResizeObserver; + + leftPaginationEnabled = false; + rightPaginationEnabled = false; + + private _scrollDistance = 0; + private _scrollDistanceChanged: boolean; + + get scrollDistance(): number { + return this._scrollDistance; + } + set scrollDistance(value: number) { + this._scrollTo(value); + } @Input() value: any; @@ -122,7 +159,22 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterC @Input() @coerceBoolean() - useSelectOnMdLg = true; + disablePagination = false; + + @Input() + selectMediaBreakpoint = 'md-lg'; + + @Input() + @coerceBoolean() + set useSelectOnMdLg(value: boolean) { + if (value) { + this.selectMediaBreakpoint = 'md-lg'; + } else { + if (this.selectMediaBreakpoint === 'md-lg') { + this.selectMediaBreakpoint = ''; + } + } + } @Input() @coerceBoolean() @@ -135,28 +187,210 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterC @coerceBoolean() disabled = false; - isMdLg: boolean; + get isMdLg(): boolean { + return !this.ignoreMdLgSize && this.isMdLgValue; + } + + private isMdLgValue: boolean; + private useSelectSubject = new BehaviorSubject(false); + + useSelect$ = this.useSelectSubject.asObservable(); private observeBreakpointSubscription: Subscription; constructor(protected store: Store, private cd: ChangeDetectorRef, + private platform: Platform, private breakpointObserver: BreakpointObserver) { super(store); } ngOnInit() { - this.isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']); + const mediaBreakpoints = [MediaBreakpoints['md-lg']]; + if (this.selectMediaBreakpoint && this.selectMediaBreakpoint !== 'md-lg') { + mediaBreakpoints.push(MediaBreakpoints[this.selectMediaBreakpoint]); + } this.observeBreakpointSubscription = this.breakpointObserver - .observe(MediaBreakpoints['md-lg']) + .observe(mediaBreakpoints) .subscribe((state: BreakpointState) => { - this.isMdLg = state.matches; + this.isMdLgValue = state.breakpoints[MediaBreakpoints['md-lg']]; + if (this.selectMediaBreakpoint) { + this.useSelectSubject.next(state.breakpoints[MediaBreakpoints[this.selectMediaBreakpoint]]); + } else { + this.useSelectSubject.next(false); + } this.cd.markForCheck(); } ); + if (!this.disablePagination) { + this.valueChange.pipe(takeUntil(this._destroyed)).subscribe(() => { + this.scrollToToggleOptionValue(); + }); + } + } + + ngOnDestroy() { + this.stopObservePagination(); + super.ngOnDestroy(); + } + + ngAfterViewInit() { + if (!this.disablePagination) { + this.useSelect$.pipe(takeUntil(this._destroyed)).subscribe((useSelect) => { + if (useSelect) { + this.removePagination(); + } else { + setTimeout(() => { + this.startObservePagination(); + }, 0); + } + }); + } + } + + ngAfterContentChecked() { + if (this._scrollDistanceChanged) { + this.updateToggleHeaderScrollPosition(); + this._scrollDistanceChanged = false; + this.cd.markForCheck(); + } + } + + ngAfterViewChecked() { + if (this._showPaginationControlsChanged) { + this.scrollToToggleOptionValue(); + this._showPaginationControlsChanged = false; + this.cd.markForCheck(); + } } trackByHeaderOption(index: number, option: ToggleHeaderOption){ return option.value; } + + handlePaginatorClick(direction: ScrollDirection, $event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.scrollHeader(direction); + } + + handlePaginatorTouchStart(direction: ScrollDirection, $event: Event) { + if (direction === 'before' && !this.leftPaginationEnabled || + direction === 'after' && !this.rightPaginationEnabled) { + $event.preventDefault(); + } + } + + private startObservePagination() { + this.toggleGroupResize$ = new ResizeObserver(() => { + this.updatePagination(); + }); + this.toggleGroupResize$.observe(this.toggleGroupContainer.nativeElement); + } + + private removePagination() { + this.stopObservePagination(); + this.showPaginationControls = false; + } + + private stopObservePagination() { + if (this.toggleGroupResize$) { + this.toggleGroupResize$.disconnect(); + this.toggleGroupResize$ = null; + } + } + + private scrollHeader(direction: ScrollDirection) { + const viewLength = this.toggleGroup.nativeElement.offsetWidth; + // Move the scroll distance one-third the length of the tab list's viewport. + const scrollAmount = ((direction === 'before' ? -1 : 1) * viewLength) / 3; + return this._scrollTo(this._scrollDistance + scrollAmount); + } + + private scrollToToggleOptionValue() { + if (this.buttonToggleGroup && this.buttonToggleGroup.selected) { + const selectedToggleButton = this.buttonToggleGroup.selected as MatButtonToggle; + const index = this.options.findIndex(o => o.value === selectedToggleButton.value); + const isLast = index === this.options.length - 1; + const isFirst = index === 0; + const viewLength = this.toggleGroupContainer.nativeElement.offsetWidth; + const {offsetLeft, offsetWidth} = (selectedToggleButton._buttonElement.nativeElement.offsetParent as HTMLElement); + const labelBeforePos = isFirst ? 0 : offsetLeft; + const labelAfterPos = isLast ? this.toggleGroup.nativeElement.scrollWidth : labelBeforePos + offsetWidth; + const beforeVisiblePos = this.scrollDistance; + const afterVisiblePos = this.scrollDistance + viewLength; + if (labelBeforePos < beforeVisiblePos) { + this.scrollDistance -= beforeVisiblePos - labelBeforePos; + } else if (labelAfterPos > afterVisiblePos) { + this.scrollDistance += Math.min( + labelAfterPos - afterVisiblePos, + labelBeforePos - beforeVisiblePos, + ); + } + } + } + + private updatePagination() { + this.checkPaginationEnabled(); + this.checkPaginationControls(); + this.updateToggleHeaderScrollPosition(); + } + + private checkPaginationEnabled() { + if (this.toggleGroupContainer) { + const isEnabled = this.toggleGroup.nativeElement.scrollWidth > this.toggleGroupContainer.nativeElement.offsetWidth; + if (isEnabled !== this.showPaginationControls) { + if (!isEnabled) { + this.scrollDistance = 0; + } else { + this._showPaginationControlsChanged = true; + } + this.cd.markForCheck(); + this.showPaginationControls = isEnabled; + } + } else { + this.showPaginationControls = false; + } + } + + private checkPaginationControls() { + if (!this.showPaginationControls) { + this.leftPaginationEnabled = this.rightPaginationEnabled = false; + } else { + // Check if the pagination arrows should be activated. + this.leftPaginationEnabled = this.scrollDistance > 0; + this.rightPaginationEnabled = this.scrollDistance < this.getMaxScrollDistance(); + this.cd.markForCheck(); + } + } + + private getMaxScrollDistance(): number { + const lengthOfToggleGroup = this.toggleGroup.nativeElement.scrollWidth; + const viewLength = this.toggleGroupContainer.nativeElement.offsetWidth; + return lengthOfToggleGroup - viewLength || 0; + } + + private _scrollTo(position: number) { + if (!this.showPaginationControls) { + return {maxScrollDistance: 0, distance: 0}; + } else { + const maxScrollDistance = this.getMaxScrollDistance(); + this._scrollDistance = Math.max(0, Math.min(maxScrollDistance, position)); + this._scrollDistanceChanged = true; + this.checkPaginationControls(); + return {maxScrollDistance, distance: this._scrollDistance}; + } + } + + private updateToggleHeaderScrollPosition() { + if (this.toggleGroupContainer) { + const scrollDistance = this.scrollDistance; + const translateX = -scrollDistance; + this.toggleGroup.nativeElement.style.transform = `translateX(${Math.round(translateX)}px)`; + if (this.platform.TRIDENT || this.platform.EDGE) { + this.toggleGroupContainer.nativeElement.scrollLeft = 0; + } + } + } } diff --git a/ui-ngx/src/app/shared/components/toggle-select.component.html b/ui-ngx/src/app/shared/components/toggle-select.component.html index a5ce7778b5..c03e0cfcbf 100644 --- a/ui-ngx/src/app/shared/components/toggle-select.component.html +++ b/ui-ngx/src/app/shared/components/toggle-select.component.html @@ -20,6 +20,8 @@ useSelectOnMdLg="false" [disabled]="disabled" [appearance]="appearance" + [disablePagination]="disablePagination" + [selectMediaBreakpoint]="selectMediaBreakpoint" [options]="options" [value]="modelValue" (valueChange)="updateModel($event)"> diff --git a/ui-ngx/src/app/shared/components/toggle-select.component.ts b/ui-ngx/src/app/shared/components/toggle-select.component.ts index 8338e04f6a..c1541d1128 100644 --- a/ui-ngx/src/app/shared/components/toggle-select.component.ts +++ b/ui-ngx/src/app/shared/components/toggle-select.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input } from '@angular/core'; +import { Component, forwardRef, HostBinding, Input } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @@ -35,13 +35,23 @@ import { coerceBoolean } from '@shared/decorators/coercion'; }) export class ToggleSelectComponent extends _ToggleBase implements ControlValueAccessor { + @HostBinding('style.maxWidth') + get maxWidth() { return '100%'; } + @Input() @coerceBoolean() disabled: boolean; + @Input() + selectMediaBreakpoint; + @Input() appearance: ToggleHeaderAppearance = 'stroked'; + @Input() + @coerceBoolean() + disablePagination = false; + modelValue: any; private propagateChange = null; diff --git a/ui-ngx/src/app/shared/components/unit-input.component.html b/ui-ngx/src/app/shared/components/unit-input.component.html index 3686796dbf..c15b232444 100644 --- a/ui-ngx/src/app/shared/components/unit-input.component.html +++ b/ui-ngx/src/app/shared/components/unit-input.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + = units.map(u => ({symbol: u.symbol, - name: this.translate.instant(u.name), - tags: u.tags})); + private fetchUnits$: Observable> = null; private propagateChange = (_val: any) => {}; - constructor(private fb: UntypedFormBuilder, + constructor(private fb: FormBuilder, + private resourcesService: ResourcesService, private translate: TranslateService) { } @@ -68,22 +79,22 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit { tap(value => { this.updateView(value); }), - startWith(''), map(value => (value as Unit)?.symbol ? (value as Unit).symbol : (value ? value as string : '')), - mergeMap(symbol => this.fetchUnits(symbol) ) + mergeMap(symbol => this.fetchUnits(symbol)) ); } writeValue(symbol?: string): void { this.searchText = ''; this.modelValue = symbol; - let res: Unit | string = null; - if (symbol) { - const unit = unitBySymbol(symbol); - res = unit ? unit : symbol; - } - this.unitsFormControl.patchValue(res, {emitEvent: false}); - this.dirty = true; + of(symbol).pipe( + switchMap(value => value + ? this.unitsConstant().pipe(map(units => unitBySymbol(units, value) ?? value)) + : of(null)) + ).subscribe(result => { + this.unitsFormControl.patchValue(result, {emitEvent: false}); + this.dirty = true; + }); } onFocus() { @@ -114,12 +125,9 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit { fetchUnits(searchText?: string): Observable> { this.searchText = searchText; - const result = searchUnits(this.translatedUnits, searchText); - if (result.length) { - return of(result); - } else { - return of([]); - } + return this.unitsConstant().pipe( + map(unit => searchUnits(unit, searchText)) + ); } registerOnChange(fn: any): void { @@ -145,4 +153,18 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit { this.unitInput.nativeElement.focus(); }, 0); } + + private unitsConstant(): Observable> { + if (this.fetchUnits$ === null) { + this.fetchUnits$ = getUnits(this.resourcesService).pipe( + map(units => units.map(u => ({ + symbol: u.symbol, + name: this.translate.instant(u.name), + tags: u.tags + }))), + shareReplay(1) + ); + } + return this.fetchUnits$; + } } diff --git a/ui-ngx/src/app/shared/components/user-menu.component.html b/ui-ngx/src/app/shared/components/user-menu.component.html index a25a0fec25..afb6e66437 100644 --- a/ui-ngx/src/app/shared/components/user-menu.component.html +++ b/ui-ngx/src/app/shared/components/user-menu.component.html @@ -28,13 +28,9 @@
- -